@signalsandsorcery/plugin-sdk 2.35.5 → 2.35.7
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.d.mts +341 -4
- package/dist/index.d.ts +341 -4
- package/dist/index.js +501 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +485 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,6 +43,9 @@ __export(index_exports, {
|
|
|
43
43
|
DownloadPackButton: () => DownloadPackButton,
|
|
44
44
|
EMPTY_FX_DETAIL_STATE: () => EMPTY_FX_DETAIL_STATE,
|
|
45
45
|
EMPTY_FX_STATE: () => EMPTY_FX_STATE,
|
|
46
|
+
ENSEMBLE_MAX_VOICES: () => ENSEMBLE_MAX_VOICES,
|
|
47
|
+
ENSEMBLE_MIN_VOICES: () => ENSEMBLE_MIN_VOICES,
|
|
48
|
+
ENSEMBLE_STYLES: () => ENSEMBLE_STYLES,
|
|
46
49
|
EQUAL_POWER_GAIN: () => EQUAL_POWER_GAIN,
|
|
47
50
|
FX_CATEGORIES: () => FX_CATEGORIES,
|
|
48
51
|
FX_CHAIN_ORDER: () => FX_CHAIN_ORDER,
|
|
@@ -58,6 +61,7 @@ __export(index_exports, {
|
|
|
58
61
|
ImportTrackModal: () => ImportTrackModal,
|
|
59
62
|
InstrumentDrawer: () => TrackDrawer,
|
|
60
63
|
LevelMeter: () => LevelMeter,
|
|
64
|
+
MIN_NOTE_DURATION_BEATS: () => MIN_NOTE_DURATION_BEATS,
|
|
61
65
|
Modal: () => Modal,
|
|
62
66
|
OffsetScrubber: () => OffsetScrubber,
|
|
63
67
|
PLUGIN_SDK_VERSION: () => PLUGIN_SDK_VERSION,
|
|
@@ -69,6 +73,8 @@ __export(index_exports, {
|
|
|
69
73
|
RESIZE_HANDLE_PX: () => RESIZE_HANDLE_PX,
|
|
70
74
|
ROW_HEIGHT: () => ROW_HEIGHT,
|
|
71
75
|
SLIDER_UNITY: () => SLIDER_UNITY,
|
|
76
|
+
STYLE_RULES: () => STYLE_RULES,
|
|
77
|
+
SUBMIT_ENSEMBLE_TOOL_NAME: () => SUBMIT_ENSEMBLE_TOOL_NAME,
|
|
72
78
|
SamplePackCTACard: () => SamplePackCTACard,
|
|
73
79
|
ScrollingWaveform: () => ScrollingWaveform,
|
|
74
80
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
@@ -81,6 +87,7 @@ __export(index_exports, {
|
|
|
81
87
|
TransitionDesigner: () => TransitionDesigner,
|
|
82
88
|
VolumeSlider: () => VolumeSlider,
|
|
83
89
|
WaveformView: () => WaveformView,
|
|
90
|
+
analyzeEnsemble: () => analyzeEnsemble,
|
|
84
91
|
analyzeWavPeak: () => analyzeWavPeak,
|
|
85
92
|
asAudioEffect: () => asAudioEffect,
|
|
86
93
|
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
@@ -88,8 +95,11 @@ __export(index_exports, {
|
|
|
88
95
|
asTransitionDesignerDraft: () => asTransitionDesignerDraft,
|
|
89
96
|
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
90
97
|
buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
|
|
98
|
+
buildEnsembleSystemPrompt: () => buildEnsembleSystemPrompt,
|
|
91
99
|
buildFadeVolumeCurve: () => buildFadeVolumeCurve,
|
|
92
100
|
buildRowSlots: () => buildRowSlots,
|
|
101
|
+
buildSubmitEnsembleParameters: () => buildSubmitEnsembleParameters,
|
|
102
|
+
buildViolationRetrySuffix: () => buildViolationRetrySuffix,
|
|
93
103
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
94
104
|
cellToPx: () => cellToPx,
|
|
95
105
|
centerScrollTop: () => centerScrollTop,
|
|
@@ -98,15 +108,21 @@ __export(index_exports, {
|
|
|
98
108
|
dbIdsFromKeys: () => dbIdsFromKeys,
|
|
99
109
|
dbToSlider: () => dbToSlider,
|
|
100
110
|
defaultFadeGesture: () => defaultFadeGesture,
|
|
111
|
+
defaultVoiceSpecs: () => defaultVoiceSpecs,
|
|
112
|
+
describeViolations: () => describeViolations,
|
|
101
113
|
drawWaveform: () => drawWaveform,
|
|
114
|
+
enforceVoice: () => enforceVoice,
|
|
115
|
+
foldPitchToRegister: () => foldPitchToRegister,
|
|
102
116
|
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
103
117
|
hashString: () => hashString,
|
|
104
118
|
moveItem: () => moveItem,
|
|
119
|
+
nearestPitchWithPc: () => nearestPitchWithPc,
|
|
105
120
|
newTrackState: () => newTrackState,
|
|
106
121
|
normalizeSlots: () => normalizeSlots,
|
|
107
122
|
padPair: () => padPair,
|
|
108
123
|
padSlots: () => padSlots,
|
|
109
124
|
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
125
|
+
parseEnsembleArgs: () => parseEnsembleArgs,
|
|
110
126
|
parseFades: () => parseFades,
|
|
111
127
|
parseLLMNoteResponse: () => parseLLMNoteResponse,
|
|
112
128
|
parseTrackGroups: () => parseTrackGroups,
|
|
@@ -7162,6 +7178,14 @@ function useGeneratorPanelCore({
|
|
|
7162
7178
|
...adapter.createTrackOptions()
|
|
7163
7179
|
});
|
|
7164
7180
|
setTracks((prev) => [...prev, newTrackState(handle)]);
|
|
7181
|
+
if (adapter.onTrackCreated) {
|
|
7182
|
+
try {
|
|
7183
|
+
await adapter.onTrackCreated(handle, { activeSceneId, trackDataKey });
|
|
7184
|
+
} catch (err) {
|
|
7185
|
+
console.warn(`[${logTag}] onTrackCreated failed (non-fatal):`, err);
|
|
7186
|
+
}
|
|
7187
|
+
await loadTracks(true);
|
|
7188
|
+
}
|
|
7165
7189
|
onExpandSelf?.();
|
|
7166
7190
|
setTimeout(() => {
|
|
7167
7191
|
const inputs = document.querySelectorAll(
|
|
@@ -7178,7 +7202,7 @@ function useGeneratorPanelCore({
|
|
|
7178
7202
|
isAddingTrackRef.current = false;
|
|
7179
7203
|
setIsAddingTrack(false);
|
|
7180
7204
|
}
|
|
7181
|
-
}, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
|
|
7205
|
+
}, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf, loadTracks, logTag]);
|
|
7182
7206
|
const handlePortTrack = (0, import_react30.useCallback)(
|
|
7183
7207
|
async (sel) => {
|
|
7184
7208
|
if (!activeSceneId) {
|
|
@@ -7218,6 +7242,12 @@ function useGeneratorPanelCore({
|
|
|
7218
7242
|
});
|
|
7219
7243
|
}
|
|
7220
7244
|
await adapter.applyPortedTrackSound(handle, sel.role);
|
|
7245
|
+
if (adapter.onTrackCreated) {
|
|
7246
|
+
try {
|
|
7247
|
+
await adapter.onTrackCreated(handle, { activeSceneId, trackDataKey });
|
|
7248
|
+
} catch {
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7221
7251
|
host.showToast(
|
|
7222
7252
|
"success",
|
|
7223
7253
|
`Imported to ${identity.familyKey}`,
|
|
@@ -8595,31 +8625,447 @@ function createSurgeSoundAdapter(host, overrides = {}) {
|
|
|
8595
8625
|
};
|
|
8596
8626
|
}
|
|
8597
8627
|
|
|
8628
|
+
// src/ensemble-core/voice-spec.ts
|
|
8629
|
+
var TOP = {
|
|
8630
|
+
label: "high florid line",
|
|
8631
|
+
role: "lead",
|
|
8632
|
+
registerLow: 72,
|
|
8633
|
+
registerHigh: 96,
|
|
8634
|
+
maxNotesPerBar: 8,
|
|
8635
|
+
rhythmPalette: "8ths and 16ths; melisma and short runs welcome",
|
|
8636
|
+
harmonicDiscipline: "freest voice \u2014 non-chord tones as passing/neighbor tones on weak beats, resolving by step",
|
|
8637
|
+
monoPreference: "high"
|
|
8638
|
+
};
|
|
8639
|
+
var COUNTER = {
|
|
8640
|
+
label: "countermelody",
|
|
8641
|
+
role: "strings",
|
|
8642
|
+
registerLow: 65,
|
|
8643
|
+
registerHigh: 86,
|
|
8644
|
+
maxNotesPerBar: 6,
|
|
8645
|
+
rhythmPalette: "8ths and quarters; move when the top voice rests",
|
|
8646
|
+
harmonicDiscipline: "mostly chord tones; may imitate the top voice's motifs a bar later",
|
|
8647
|
+
monoPreference: "high"
|
|
8648
|
+
};
|
|
8649
|
+
var INNER = {
|
|
8650
|
+
label: "inner voice",
|
|
8651
|
+
role: "strings",
|
|
8652
|
+
registerLow: 55,
|
|
8653
|
+
registerHigh: 76,
|
|
8654
|
+
maxNotesPerBar: 4,
|
|
8655
|
+
rhythmPalette: "quarters and halves",
|
|
8656
|
+
harmonicDiscipline: "chord tones with smooth stepwise motion between them",
|
|
8657
|
+
monoPreference: "high"
|
|
8658
|
+
};
|
|
8659
|
+
var INNER_2 = {
|
|
8660
|
+
label: "second inner voice",
|
|
8661
|
+
role: "strings",
|
|
8662
|
+
registerLow: 60,
|
|
8663
|
+
registerHigh: 81,
|
|
8664
|
+
maxNotesPerBar: 5,
|
|
8665
|
+
rhythmPalette: "quarters with occasional 8th-note motion",
|
|
8666
|
+
harmonicDiscipline: "chord tones; fill gaps the other inner voice leaves",
|
|
8667
|
+
monoPreference: "high"
|
|
8668
|
+
};
|
|
8669
|
+
var TENOR = {
|
|
8670
|
+
label: "low counterline",
|
|
8671
|
+
role: "strings",
|
|
8672
|
+
registerLow: 43,
|
|
8673
|
+
registerHigh: 64,
|
|
8674
|
+
maxNotesPerBar: 3,
|
|
8675
|
+
rhythmPalette: "quarters and halves; brief walking figures at cadences",
|
|
8676
|
+
harmonicDiscipline: "roots and fifths emphasized; passing tones only between chord tones",
|
|
8677
|
+
monoPreference: "low"
|
|
8678
|
+
};
|
|
8679
|
+
var BASS = {
|
|
8680
|
+
label: "bassline",
|
|
8681
|
+
role: "bass",
|
|
8682
|
+
registerLow: 36,
|
|
8683
|
+
registerHigh: 60,
|
|
8684
|
+
maxNotesPerBar: 3,
|
|
8685
|
+
rhythmPalette: "quarters and halves",
|
|
8686
|
+
harmonicDiscipline: "chord roots and fifths; stepwise approaches into chord changes",
|
|
8687
|
+
monoPreference: "low"
|
|
8688
|
+
};
|
|
8689
|
+
var SUB = {
|
|
8690
|
+
label: "sub anchor",
|
|
8691
|
+
role: "808s",
|
|
8692
|
+
registerLow: 24,
|
|
8693
|
+
registerHigh: 43,
|
|
8694
|
+
maxNotesPerBar: 2,
|
|
8695
|
+
rhythmPalette: "halves and whole notes; sustain into the bar",
|
|
8696
|
+
harmonicDiscipline: "ROOT pitch class only \u2014 the harmonic anchor",
|
|
8697
|
+
rootOnly: true,
|
|
8698
|
+
monoPreference: "low"
|
|
8699
|
+
};
|
|
8700
|
+
var ENSEMBLE_MIN_VOICES = 2;
|
|
8701
|
+
var ENSEMBLE_MAX_VOICES = 6;
|
|
8702
|
+
var SPEC_TABLES = {
|
|
8703
|
+
2: [TOP, BASS],
|
|
8704
|
+
3: [TOP, INNER, BASS],
|
|
8705
|
+
4: [TOP, COUNTER, TENOR, BASS],
|
|
8706
|
+
5: [TOP, COUNTER, INNER, TENOR, SUB],
|
|
8707
|
+
6: [TOP, COUNTER, INNER_2, INNER, TENOR, SUB]
|
|
8708
|
+
};
|
|
8709
|
+
function defaultVoiceSpecs(voiceCount) {
|
|
8710
|
+
const n = Math.max(ENSEMBLE_MIN_VOICES, Math.min(ENSEMBLE_MAX_VOICES, Math.round(voiceCount)));
|
|
8711
|
+
return SPEC_TABLES[n].map((spec, voiceIndex) => ({ ...spec, voiceIndex }));
|
|
8712
|
+
}
|
|
8713
|
+
|
|
8714
|
+
// src/ensemble-core/enforce-voice.ts
|
|
8715
|
+
var MIN_NOTE_DURATION_BEATS = 0.0625;
|
|
8716
|
+
var BEATS_PER_BAR = 4;
|
|
8717
|
+
function foldPitchToRegister(pitch, low, high) {
|
|
8718
|
+
let p = pitch;
|
|
8719
|
+
while (p < low) p += 12;
|
|
8720
|
+
while (p > high) p -= 12;
|
|
8721
|
+
if (p < low) p = low;
|
|
8722
|
+
if (p > high) p = high;
|
|
8723
|
+
return p;
|
|
8724
|
+
}
|
|
8725
|
+
function nearestPitchWithPc(reference, pc, low, high) {
|
|
8726
|
+
let best = null;
|
|
8727
|
+
for (let p = low; p <= high; p++) {
|
|
8728
|
+
if ((p % 12 + 12) % 12 !== pc) continue;
|
|
8729
|
+
if (best === null || Math.abs(p - reference) < Math.abs(best - reference)) best = p;
|
|
8730
|
+
}
|
|
8731
|
+
return best ?? foldPitchToRegister(reference, low, high);
|
|
8732
|
+
}
|
|
8733
|
+
function snapToNearestPc(pitch, pcs) {
|
|
8734
|
+
if (pcs.size === 0) return pitch;
|
|
8735
|
+
for (let d = 0; d <= 6; d++) {
|
|
8736
|
+
if (pcs.has(((pitch - d) % 12 + 12) % 12)) return pitch - d;
|
|
8737
|
+
if (pcs.has(((pitch + d) % 12 + 12) % 12)) return pitch + d;
|
|
8738
|
+
}
|
|
8739
|
+
return pitch;
|
|
8740
|
+
}
|
|
8741
|
+
function enforceVoice(rawNotes, spec, opts) {
|
|
8742
|
+
const repairs = [];
|
|
8743
|
+
const clipEnd = opts.bars * BEATS_PER_BAR;
|
|
8744
|
+
let notes = [];
|
|
8745
|
+
for (const n of rawNotes) {
|
|
8746
|
+
if (!Number.isFinite(n.pitch) || !Number.isFinite(n.startBeat) || !Number.isFinite(n.durationBeats)) continue;
|
|
8747
|
+
if (n.startBeat >= clipEnd || n.startBeat < 0) {
|
|
8748
|
+
repairs.push(`voice ${spec.voiceIndex}: dropped note outside the ${opts.bars}-bar clip (start ${n.startBeat})`);
|
|
8749
|
+
continue;
|
|
8750
|
+
}
|
|
8751
|
+
const durationBeats = Math.max(
|
|
8752
|
+
MIN_NOTE_DURATION_BEATS,
|
|
8753
|
+
Math.min(n.durationBeats, clipEnd - n.startBeat)
|
|
8754
|
+
);
|
|
8755
|
+
notes.push({ ...n, durationBeats });
|
|
8756
|
+
}
|
|
8757
|
+
notes = notes.map((n) => {
|
|
8758
|
+
const folded = foldPitchToRegister(Math.round(n.pitch), spec.registerLow, spec.registerHigh);
|
|
8759
|
+
if (folded !== n.pitch) {
|
|
8760
|
+
repairs.push(`voice ${spec.voiceIndex}: folded pitch ${n.pitch} into register ${spec.registerLow}-${spec.registerHigh} (${folded})`);
|
|
8761
|
+
}
|
|
8762
|
+
return { ...n, pitch: folded };
|
|
8763
|
+
});
|
|
8764
|
+
if (spec.rootOnly && opts.chordRootPcAtBar) {
|
|
8765
|
+
notes = notes.map((n) => {
|
|
8766
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8767
|
+
const rootPc = opts.chordRootPcAtBar(bar);
|
|
8768
|
+
if (rootPc === null) return n;
|
|
8769
|
+
const pinned = nearestPitchWithPc(n.pitch, rootPc, spec.registerLow, spec.registerHigh);
|
|
8770
|
+
if (pinned !== n.pitch) {
|
|
8771
|
+
repairs.push(`voice ${spec.voiceIndex}: pinned bar ${bar + 1} note to the chord root (${n.pitch} \u2192 ${pinned})`);
|
|
8772
|
+
}
|
|
8773
|
+
return { ...n, pitch: pinned };
|
|
8774
|
+
});
|
|
8775
|
+
}
|
|
8776
|
+
if (opts.scalePcs && opts.scalePcs.size > 0 && !spec.rootOnly) {
|
|
8777
|
+
notes = notes.map((n) => {
|
|
8778
|
+
const pc = (n.pitch % 12 + 12) % 12;
|
|
8779
|
+
if (opts.scalePcs.has(pc)) return n;
|
|
8780
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8781
|
+
const chordPcs = opts.chordPcsAtBar?.(bar);
|
|
8782
|
+
if (chordPcs?.has(pc)) return n;
|
|
8783
|
+
const snapped = foldPitchToRegister(
|
|
8784
|
+
snapToNearestPc(n.pitch, opts.scalePcs),
|
|
8785
|
+
spec.registerLow,
|
|
8786
|
+
spec.registerHigh
|
|
8787
|
+
);
|
|
8788
|
+
if (snapped !== n.pitch) {
|
|
8789
|
+
repairs.push(`voice ${spec.voiceIndex}: snapped out-of-key pitch ${n.pitch} \u2192 ${snapped}`);
|
|
8790
|
+
}
|
|
8791
|
+
return { ...n, pitch: snapped };
|
|
8792
|
+
});
|
|
8793
|
+
}
|
|
8794
|
+
notes.sort((a, b) => a.startBeat - b.startBeat || (spec.monoPreference === "high" ? b.pitch - a.pitch : a.pitch - b.pitch));
|
|
8795
|
+
const mono = [];
|
|
8796
|
+
for (const n of notes) {
|
|
8797
|
+
const prev = mono[mono.length - 1];
|
|
8798
|
+
if (prev && Math.abs(prev.startBeat - n.startBeat) < 1e-9) {
|
|
8799
|
+
repairs.push(`voice ${spec.voiceIndex}: dropped simultaneous note ${n.pitch} at beat ${n.startBeat} (voice is one line)`);
|
|
8800
|
+
continue;
|
|
8801
|
+
}
|
|
8802
|
+
if (prev && prev.startBeat + prev.durationBeats > n.startBeat) {
|
|
8803
|
+
prev.durationBeats = Math.max(MIN_NOTE_DURATION_BEATS, n.startBeat - prev.startBeat);
|
|
8804
|
+
}
|
|
8805
|
+
mono.push({ ...n });
|
|
8806
|
+
}
|
|
8807
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
8808
|
+
for (const n of mono) {
|
|
8809
|
+
const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
|
|
8810
|
+
const bucket = byBar.get(bar) ?? [];
|
|
8811
|
+
bucket.push(n);
|
|
8812
|
+
byBar.set(bar, bucket);
|
|
8813
|
+
}
|
|
8814
|
+
const kept = new Set(mono);
|
|
8815
|
+
for (const [bar, bucket] of byBar) {
|
|
8816
|
+
if (bucket.length <= spec.maxNotesPerBar) continue;
|
|
8817
|
+
const strength = (n) => {
|
|
8818
|
+
const beatInBar = n.startBeat - bar * BEATS_PER_BAR;
|
|
8819
|
+
const onDownbeat = Math.abs(beatInBar % 1) < 1e-9 ? 1 : 0;
|
|
8820
|
+
return n.durationBeats * 4 + n.velocity / 127 + onDownbeat * 2;
|
|
8821
|
+
};
|
|
8822
|
+
const ranked = [...bucket].sort((a, b) => strength(a) - strength(b));
|
|
8823
|
+
const excess = bucket.length - spec.maxNotesPerBar;
|
|
8824
|
+
for (let i = 0; i < excess; i++) {
|
|
8825
|
+
kept.delete(ranked[i]);
|
|
8826
|
+
}
|
|
8827
|
+
repairs.push(`voice ${spec.voiceIndex}: thinned bar ${bar + 1} from ${bucket.length} to ${spec.maxNotesPerBar} notes (density cap)`);
|
|
8828
|
+
}
|
|
8829
|
+
return { notes: mono.filter((n) => kept.has(n)), repairs };
|
|
8830
|
+
}
|
|
8831
|
+
|
|
8832
|
+
// src/ensemble-core/analyze-ensemble.ts
|
|
8833
|
+
var EPS = 1e-6;
|
|
8834
|
+
function pitchAt(voice, beat) {
|
|
8835
|
+
for (const n of voice) {
|
|
8836
|
+
if (n.startBeat - EPS <= beat && beat < n.startBeat + n.durationBeats - EPS) return n.pitch;
|
|
8837
|
+
}
|
|
8838
|
+
return null;
|
|
8839
|
+
}
|
|
8840
|
+
function onsetSet(voice) {
|
|
8841
|
+
return [...new Set(voice.map((n) => Math.round(n.startBeat * 96) / 96))].sort((a, b) => a - b);
|
|
8842
|
+
}
|
|
8843
|
+
function analyzeEnsemble(voices) {
|
|
8844
|
+
const pairs = [];
|
|
8845
|
+
for (let i = 0; i + 1 < voices.length; i++) {
|
|
8846
|
+
const upper = voices[i];
|
|
8847
|
+
const lower = voices[i + 1];
|
|
8848
|
+
const upperOnsets = onsetSet(upper);
|
|
8849
|
+
const lowerOnsets = new Set(onsetSet(lower));
|
|
8850
|
+
const shared = upperOnsets.filter((b) => lowerOnsets.has(b));
|
|
8851
|
+
const onsetIndependence = upperOnsets.length === 0 ? 1 : 1 - shared.length / upperOnsets.length;
|
|
8852
|
+
const motion = { contrary: 0, oblique: 0, similar: 0, parallel: 0 };
|
|
8853
|
+
const parallelPerfects = [];
|
|
8854
|
+
for (let s = 0; s + 1 < shared.length; s++) {
|
|
8855
|
+
const [b0, b1] = [shared[s], shared[s + 1]];
|
|
8856
|
+
const u0 = pitchAt(upper, b0);
|
|
8857
|
+
const u1 = pitchAt(upper, b1);
|
|
8858
|
+
const l0 = pitchAt(lower, b0);
|
|
8859
|
+
const l1 = pitchAt(lower, b1);
|
|
8860
|
+
if (u0 === null || u1 === null || l0 === null || l1 === null) continue;
|
|
8861
|
+
const du = Math.sign(u1 - u0);
|
|
8862
|
+
const dl = Math.sign(l1 - l0);
|
|
8863
|
+
let kind;
|
|
8864
|
+
if (du === 0 || dl === 0) kind = "oblique";
|
|
8865
|
+
else if (du !== dl) kind = "contrary";
|
|
8866
|
+
else {
|
|
8867
|
+
kind = u1 - l1 === u0 - l0 ? "parallel" : "similar";
|
|
8868
|
+
}
|
|
8869
|
+
motion[kind] += 1;
|
|
8870
|
+
if (kind === "parallel") {
|
|
8871
|
+
const intervalPc = ((u1 - l1) % 12 + 12) % 12;
|
|
8872
|
+
if (intervalPc === 0 || intervalPc === 7) {
|
|
8873
|
+
parallelPerfects.push({ atBeat: b1, intervalPc });
|
|
8874
|
+
}
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
const crossings = [];
|
|
8878
|
+
for (const b of [...upperOnsets, ...onsetSet(lower)]) {
|
|
8879
|
+
const u = pitchAt(upper, b);
|
|
8880
|
+
const l = pitchAt(lower, b);
|
|
8881
|
+
if (u !== null && l !== null && u < l) crossings.push(b);
|
|
8882
|
+
}
|
|
8883
|
+
pairs.push({
|
|
8884
|
+
upperVoice: i,
|
|
8885
|
+
onsetIndependence,
|
|
8886
|
+
motion,
|
|
8887
|
+
parallelPerfects,
|
|
8888
|
+
crossings: [...new Set(crossings)].sort((a, b) => a - b)
|
|
8889
|
+
});
|
|
8890
|
+
}
|
|
8891
|
+
const allOnsets = voices.map(onsetSet);
|
|
8892
|
+
const union = new Set(allOnsets.flat());
|
|
8893
|
+
let sharedByAll = 0;
|
|
8894
|
+
for (const b of union) {
|
|
8895
|
+
if (allOnsets.every((o) => o.length === 0 || o.includes(b))) sharedByAll += 1;
|
|
8896
|
+
}
|
|
8897
|
+
const homorhythmScore = union.size === 0 ? 0 : sharedByAll / union.size;
|
|
8898
|
+
return { pairs, homorhythmScore };
|
|
8899
|
+
}
|
|
8900
|
+
function describeViolations(analysis, rules) {
|
|
8901
|
+
const out = [];
|
|
8902
|
+
for (const pair of analysis.pairs) {
|
|
8903
|
+
const label = `between voice ${pair.upperVoice + 1} and voice ${pair.upperVoice + 2}`;
|
|
8904
|
+
if (rules.forbidParallelPerfects) {
|
|
8905
|
+
for (const p of pair.parallelPerfects) {
|
|
8906
|
+
out.push(`Parallel ${p.intervalPc === 0 ? "octaves" : "fifths"} ${label} at beat ${p.atBeat} \u2014 approach perfect intervals by contrary or oblique motion.`);
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
if (rules.forbidVoiceCrossing && pair.crossings.length > 0) {
|
|
8910
|
+
out.push(`Voice crossing ${label} at beat${pair.crossings.length > 1 ? "s" : ""} ${pair.crossings.join(", ")} \u2014 keep each voice inside its register lane.`);
|
|
8911
|
+
}
|
|
8912
|
+
if (pair.onsetIndependence < rules.minOnsetIndependence) {
|
|
8913
|
+
out.push(`Voices ${pair.upperVoice + 1} and ${pair.upperVoice + 2} attack together too often (independence ${pair.onsetIndependence.toFixed(2)} < ${rules.minOnsetIndependence}) \u2014 stagger entrances and let one voice move while the other holds.`);
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
return out;
|
|
8917
|
+
}
|
|
8918
|
+
|
|
8919
|
+
// src/ensemble-core/styles.ts
|
|
8920
|
+
var ENSEMBLE_STYLES = ["counterpoint", "chorale", "interlock"];
|
|
8921
|
+
var STYLE_RULES = {
|
|
8922
|
+
counterpoint: {
|
|
8923
|
+
forbidParallelPerfects: true,
|
|
8924
|
+
forbidVoiceCrossing: true,
|
|
8925
|
+
minOnsetIndependence: 0.35,
|
|
8926
|
+
promptParagraph: "STYLE \u2014 COUNTERPOINT (modern, not strict species): independent singable lines. Favor contrary and oblique motion between neighbors; approach perfect intervals by contrary motion; imitate motifs between voices a bar apart; stagger entrances so voices converse instead of speaking at once."
|
|
8927
|
+
},
|
|
8928
|
+
chorale: {
|
|
8929
|
+
forbidParallelPerfects: true,
|
|
8930
|
+
forbidVoiceCrossing: true,
|
|
8931
|
+
minOnsetIndependence: 0,
|
|
8932
|
+
promptParagraph: "STYLE \u2014 CHORALE: homorhythmic block harmony. Voices move together on the same rhythm with smooth voice-leading \u2014 nearest chord tone, common tones held, no leaps larger than a fifth in inner voices."
|
|
8933
|
+
},
|
|
8934
|
+
interlock: {
|
|
8935
|
+
forbidParallelPerfects: false,
|
|
8936
|
+
forbidVoiceCrossing: false,
|
|
8937
|
+
minOnsetIndependence: 0.6,
|
|
8938
|
+
promptParagraph: "STYLE \u2014 INTERLOCK (minimal / systems music): short repeating cells that mesh like gears. Each voice keeps its own ostinato; onsets rarely coincide with the neighboring voice; parallel motion and doubling are welcome when the composite rhythm stays busy and even."
|
|
8939
|
+
}
|
|
8940
|
+
};
|
|
8941
|
+
|
|
8942
|
+
// src/ensemble-core/ensemble-schema.ts
|
|
8943
|
+
var SUBMIT_ENSEMBLE_TOOL_NAME = "submit_ensemble";
|
|
8944
|
+
function buildSubmitEnsembleParameters(voiceCount) {
|
|
8945
|
+
return {
|
|
8946
|
+
type: "object",
|
|
8947
|
+
properties: {
|
|
8948
|
+
voices: {
|
|
8949
|
+
type: "array",
|
|
8950
|
+
description: `Exactly ${voiceCount} voices, voiceIndex 0 (top) through ${voiceCount - 1} (bottom).`,
|
|
8951
|
+
items: {
|
|
8952
|
+
type: "object",
|
|
8953
|
+
properties: {
|
|
8954
|
+
voiceIndex: { type: "integer", description: "0 = top voice" },
|
|
8955
|
+
notes: {
|
|
8956
|
+
type: "array",
|
|
8957
|
+
items: {
|
|
8958
|
+
type: "object",
|
|
8959
|
+
properties: {
|
|
8960
|
+
pitch: { type: "integer", description: "MIDI note number 0-127" },
|
|
8961
|
+
startBeat: { type: "number", description: "quarter-note beats from clip start" },
|
|
8962
|
+
durationBeats: { type: "number", description: "duration in quarter-note beats" },
|
|
8963
|
+
velocity: { type: "integer", description: "1-127" }
|
|
8964
|
+
},
|
|
8965
|
+
required: ["pitch", "startBeat", "durationBeats", "velocity"]
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
},
|
|
8969
|
+
required: ["voiceIndex", "notes"]
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
},
|
|
8973
|
+
required: ["voices"]
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
function parseEnsembleArgs(args, voiceCount) {
|
|
8977
|
+
if (typeof args !== "object" || args === null) return null;
|
|
8978
|
+
const voicesRaw = args.voices;
|
|
8979
|
+
if (!Array.isArray(voicesRaw)) return null;
|
|
8980
|
+
const warnings = [];
|
|
8981
|
+
const voiceNotes = Array.from({ length: voiceCount }, () => []);
|
|
8982
|
+
for (const v of voicesRaw) {
|
|
8983
|
+
if (typeof v !== "object" || v === null) continue;
|
|
8984
|
+
const idxRaw = v.voiceIndex;
|
|
8985
|
+
const idx = typeof idxRaw === "number" ? Math.round(idxRaw) : NaN;
|
|
8986
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= voiceCount) {
|
|
8987
|
+
warnings.push(`ignored voice with out-of-range voiceIndex ${String(idxRaw)}`);
|
|
8988
|
+
continue;
|
|
8989
|
+
}
|
|
8990
|
+
const notesRaw = v.notes;
|
|
8991
|
+
if (!Array.isArray(notesRaw)) continue;
|
|
8992
|
+
const notes = [];
|
|
8993
|
+
let dropped = 0;
|
|
8994
|
+
for (const n of notesRaw) {
|
|
8995
|
+
const note = n;
|
|
8996
|
+
if (typeof note?.pitch === "number" && typeof note?.startBeat === "number" && typeof note?.durationBeats === "number" && typeof note?.velocity === "number" && note.durationBeats > 0 && note.startBeat >= 0) {
|
|
8997
|
+
notes.push({
|
|
8998
|
+
pitch: Math.max(0, Math.min(127, Math.round(note.pitch))),
|
|
8999
|
+
startBeat: note.startBeat,
|
|
9000
|
+
durationBeats: note.durationBeats,
|
|
9001
|
+
velocity: Math.max(1, Math.min(127, Math.round(note.velocity)))
|
|
9002
|
+
});
|
|
9003
|
+
} else {
|
|
9004
|
+
dropped += 1;
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
if (dropped > 0) warnings.push(`voice ${idx}: dropped ${dropped} malformed note(s)`);
|
|
9008
|
+
voiceNotes[idx] = notes;
|
|
9009
|
+
}
|
|
9010
|
+
const returned = voicesRaw.length;
|
|
9011
|
+
if (returned !== voiceCount) {
|
|
9012
|
+
warnings.push(`model returned ${returned} voices for a ${voiceCount}-voice ensemble`);
|
|
9013
|
+
}
|
|
9014
|
+
const anyNotes = voiceNotes.some((v) => v.length > 0);
|
|
9015
|
+
return anyNotes ? { voiceNotes, warnings } : null;
|
|
9016
|
+
}
|
|
9017
|
+
|
|
9018
|
+
// src/ensemble-core/ensemble-prompt.ts
|
|
9019
|
+
function voiceContractLine(spec) {
|
|
9020
|
+
const root = spec.rootOnly ? " ROOT PITCH CLASS ONLY (each bar's chord root)." : "";
|
|
9021
|
+
return `- Voice ${spec.voiceIndex + 1} (${spec.label}): MIDI ${spec.registerLow}-${spec.registerHigh}, max ${spec.maxNotesPerBar} notes/bar, rhythm: ${spec.rhythmPalette}. ${spec.harmonicDiscipline}.${root}`;
|
|
9022
|
+
}
|
|
9023
|
+
function buildEnsembleSystemPrompt(specs, style) {
|
|
9024
|
+
const styleParagraph = STYLE_RULES[style].promptParagraph;
|
|
9025
|
+
return `You are an ensemble composer. Compose ${specs.length} voices as ONE piece of music \u2014 not ${specs.length} independent parts.
|
|
9026
|
+
|
|
9027
|
+
Submit your composition by calling the ${SUBMIT_ENSEMBLE_TOOL_NAME} tool with all ${specs.length} voices.
|
|
9028
|
+
|
|
9029
|
+
THE ENSEMBLE RULES (most important):
|
|
9030
|
+
1. The voices are composed TOGETHER. They must relate: imitate and answer each other's motifs, move in contrary or oblique motion against neighbors, and stagger entrances so lines converse.
|
|
9031
|
+
2. Each voice is a SINGLE monophonic line \u2014 within one voice, no two notes overlap in time and no chords. Voices MAY and SHOULD overlap EACH OTHER; that overlap is the music.
|
|
9032
|
+
3. Complexity decreases downward: the top voice is the most active and ornamented, the bottom voice the sparsest anchor. Respect each voice's register window and notes-per-bar cap exactly.
|
|
9033
|
+
4. Follow the chord progression in the musical context exactly. Non-chord tones only as passing or neighbor tones on weak beats, resolving by step.
|
|
9034
|
+
5. Not every voice plays all the time \u2014 rests are structural. Avoid all voices attacking the same beat except at phrase boundaries.
|
|
9035
|
+
6. startBeat/durationBeats are in quarter-note beats from the start of the clip; fill the stated bar length, and make bar 1 land immediately (no long silent intro).
|
|
9036
|
+
|
|
9037
|
+
${styleParagraph}
|
|
9038
|
+
|
|
9039
|
+
PER-VOICE CONTRACTS:
|
|
9040
|
+
${specs.map(voiceContractLine).join("\n")}`;
|
|
9041
|
+
}
|
|
9042
|
+
function buildViolationRetrySuffix(violations) {
|
|
9043
|
+
if (violations.length === 0) return "";
|
|
9044
|
+
return `
|
|
9045
|
+
|
|
9046
|
+
Your previous ensemble had these problems \u2014 fix them while keeping everything that worked:
|
|
9047
|
+
` + violations.map((v) => `- ${v}`).join("\n");
|
|
9048
|
+
}
|
|
9049
|
+
|
|
8598
9050
|
// src/constants/sdk-version.ts
|
|
8599
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
9051
|
+
var PLUGIN_SDK_VERSION = "2.43.0";
|
|
8600
9052
|
|
|
8601
9053
|
// src/utils/format-concurrent-tracks.ts
|
|
8602
9054
|
function formatConcurrentTracks(ctx) {
|
|
8603
9055
|
const tracks = ctx.concurrentTracks;
|
|
8604
9056
|
if (!tracks || tracks.length === 0) return "";
|
|
8605
|
-
const
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
if (track.truncated && typeof track.originalNoteCount === "number") {
|
|
8618
|
-
const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
|
|
8619
|
-
if (dropped > 0) {
|
|
8620
|
-
lines.push(` \u2026 (${dropped} more notes truncated)`);
|
|
8621
|
-
}
|
|
8622
|
-
}
|
|
9057
|
+
const pinned = tracks.filter((t) => t.pinned);
|
|
9058
|
+
const ambient = tracks.filter((t) => !t.pinned);
|
|
9059
|
+
const lines = [];
|
|
9060
|
+
if (pinned.length > 0) {
|
|
9061
|
+
lines.push(
|
|
9062
|
+
"REFERENCE TRACKS (write in counterpoint against these \u2014 interlock with their rhythm, avoid doubling their onsets, favor contrary or oblique motion):"
|
|
9063
|
+
);
|
|
9064
|
+
for (const track of pinned) pushTrackLines(lines, track);
|
|
9065
|
+
}
|
|
9066
|
+
if (ambient.length > 0) {
|
|
9067
|
+
lines.push(`Concurrent tracks in scene (already generated):`);
|
|
9068
|
+
for (const track of ambient) pushTrackLines(lines, track);
|
|
8623
9069
|
}
|
|
8624
9070
|
if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {
|
|
8625
9071
|
lines.push(
|
|
@@ -8628,6 +9074,25 @@ function formatConcurrentTracks(ctx) {
|
|
|
8628
9074
|
}
|
|
8629
9075
|
return lines.join("\n");
|
|
8630
9076
|
}
|
|
9077
|
+
function pushTrackLines(lines, track) {
|
|
9078
|
+
const nameStr = track.name ? ` name="${escapeQuotes(track.name)}"` : "";
|
|
9079
|
+
const promptStr = track.prompt ? ` prompt="${escapeQuotes(track.prompt)}"` : "";
|
|
9080
|
+
lines.push(` - role=${track.role ?? "unknown"}${nameStr}${promptStr}`);
|
|
9081
|
+
if (track.notesByChord.length === 0) {
|
|
9082
|
+
lines.push(` (no notes)`);
|
|
9083
|
+
} else {
|
|
9084
|
+
for (const segment of track.notesByChord) {
|
|
9085
|
+
if (segment.notes.length === 0) continue;
|
|
9086
|
+
lines.push(` ${formatChordSegment(segment)}`);
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
if (track.truncated && typeof track.originalNoteCount === "number") {
|
|
9090
|
+
const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
|
|
9091
|
+
if (dropped > 0) {
|
|
9092
|
+
lines.push(` \u2026 (${dropped} more notes truncated)`);
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
8631
9096
|
function formatChordSegment(segment) {
|
|
8632
9097
|
const [start, end] = segment.chordRangeQn;
|
|
8633
9098
|
const notesJson = JSON.stringify(segment.notes.map(compactNote2));
|
|
@@ -8753,6 +9218,9 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8753
9218
|
DownloadPackButton,
|
|
8754
9219
|
EMPTY_FX_DETAIL_STATE,
|
|
8755
9220
|
EMPTY_FX_STATE,
|
|
9221
|
+
ENSEMBLE_MAX_VOICES,
|
|
9222
|
+
ENSEMBLE_MIN_VOICES,
|
|
9223
|
+
ENSEMBLE_STYLES,
|
|
8756
9224
|
EQUAL_POWER_GAIN,
|
|
8757
9225
|
FX_CATEGORIES,
|
|
8758
9226
|
FX_CHAIN_ORDER,
|
|
@@ -8768,6 +9236,7 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8768
9236
|
ImportTrackModal,
|
|
8769
9237
|
InstrumentDrawer,
|
|
8770
9238
|
LevelMeter,
|
|
9239
|
+
MIN_NOTE_DURATION_BEATS,
|
|
8771
9240
|
Modal,
|
|
8772
9241
|
OffsetScrubber,
|
|
8773
9242
|
PLUGIN_SDK_VERSION,
|
|
@@ -8779,6 +9248,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8779
9248
|
RESIZE_HANDLE_PX,
|
|
8780
9249
|
ROW_HEIGHT,
|
|
8781
9250
|
SLIDER_UNITY,
|
|
9251
|
+
STYLE_RULES,
|
|
9252
|
+
SUBMIT_ENSEMBLE_TOOL_NAME,
|
|
8782
9253
|
SamplePackCTACard,
|
|
8783
9254
|
ScrollingWaveform,
|
|
8784
9255
|
SorceryProgressBar,
|
|
@@ -8791,6 +9262,7 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8791
9262
|
TransitionDesigner,
|
|
8792
9263
|
VolumeSlider,
|
|
8793
9264
|
WaveformView,
|
|
9265
|
+
analyzeEnsemble,
|
|
8794
9266
|
analyzeWavPeak,
|
|
8795
9267
|
asAudioEffect,
|
|
8796
9268
|
asCrossfadeMeta,
|
|
@@ -8798,8 +9270,11 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8798
9270
|
asTransitionDesignerDraft,
|
|
8799
9271
|
buildCrossfadeInpaintPrompt,
|
|
8800
9272
|
buildCrossfadeVolumeCurves,
|
|
9273
|
+
buildEnsembleSystemPrompt,
|
|
8801
9274
|
buildFadeVolumeCurve,
|
|
8802
9275
|
buildRowSlots,
|
|
9276
|
+
buildSubmitEnsembleParameters,
|
|
9277
|
+
buildViolationRetrySuffix,
|
|
8803
9278
|
calculateTimeBasedTarget,
|
|
8804
9279
|
cellToPx,
|
|
8805
9280
|
centerScrollTop,
|
|
@@ -8808,15 +9283,21 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
8808
9283
|
dbIdsFromKeys,
|
|
8809
9284
|
dbToSlider,
|
|
8810
9285
|
defaultFadeGesture,
|
|
9286
|
+
defaultVoiceSpecs,
|
|
9287
|
+
describeViolations,
|
|
8811
9288
|
drawWaveform,
|
|
9289
|
+
enforceVoice,
|
|
9290
|
+
foldPitchToRegister,
|
|
8812
9291
|
formatConcurrentTracks,
|
|
8813
9292
|
hashString,
|
|
8814
9293
|
moveItem,
|
|
9294
|
+
nearestPitchWithPc,
|
|
8815
9295
|
newTrackState,
|
|
8816
9296
|
normalizeSlots,
|
|
8817
9297
|
padPair,
|
|
8818
9298
|
padSlots,
|
|
8819
9299
|
parseCrossfadePairs,
|
|
9300
|
+
parseEnsembleArgs,
|
|
8820
9301
|
parseFades,
|
|
8821
9302
|
parseLLMNoteResponse,
|
|
8822
9303
|
parseTrackGroups,
|