@kano/stem-daw 0.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.
Files changed (67) hide show
  1. package/README.md +253 -0
  2. package/dist/chat-actions-54Z6URC4.js +7 -0
  3. package/dist/chat-actions-54Z6URC4.js.map +1 -0
  4. package/dist/chunk-56PWIP7O.js +1029 -0
  5. package/dist/chunk-56PWIP7O.js.map +1 -0
  6. package/dist/chunk-AAVC7KUW.js +145 -0
  7. package/dist/chunk-AAVC7KUW.js.map +1 -0
  8. package/dist/chunk-KCOOE2OP.js +1764 -0
  9. package/dist/chunk-KCOOE2OP.js.map +1 -0
  10. package/dist/chunk-LO74ZJ4H.js +23923 -0
  11. package/dist/chunk-LO74ZJ4H.js.map +1 -0
  12. package/dist/chunk-OFGZURP6.js +247 -0
  13. package/dist/chunk-OFGZURP6.js.map +1 -0
  14. package/dist/chunk-OYNES5W3.js +3085 -0
  15. package/dist/chunk-OYNES5W3.js.map +1 -0
  16. package/dist/chunk-QQ5NZTHT.js +336 -0
  17. package/dist/chunk-QQ5NZTHT.js.map +1 -0
  18. package/dist/chunk-TBXCZFAY.js +13713 -0
  19. package/dist/chunk-TBXCZFAY.js.map +1 -0
  20. package/dist/chunk-U44X6QP5.js +281 -0
  21. package/dist/chunk-U44X6QP5.js.map +1 -0
  22. package/dist/chunk-UKMELGZL.js +27 -0
  23. package/dist/chunk-UKMELGZL.js.map +1 -0
  24. package/dist/components/DAWView.d.ts +19 -0
  25. package/dist/components/DAWView.js +11 -0
  26. package/dist/components/DAWView.js.map +1 -0
  27. package/dist/daw-controller-BjRWcTol.d.ts +339 -0
  28. package/dist/engine/daw-controller.d.ts +3 -0
  29. package/dist/engine/daw-controller.js +5 -0
  30. package/dist/engine/daw-controller.js.map +1 -0
  31. package/dist/engine/daw-import-stem-fm-config.d.ts +224 -0
  32. package/dist/engine/daw-import-stem-fm-config.js +7 -0
  33. package/dist/engine/daw-import-stem-fm-config.js.map +1 -0
  34. package/dist/fetchStationTracks-SKFT4V3U.js +3 -0
  35. package/dist/fetchStationTracks-SKFT4V3U.js.map +1 -0
  36. package/dist/index.d.ts +308 -0
  37. package/dist/index.js +332 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/interface-DaRj7RkY.d.ts +66 -0
  40. package/dist/interfaces-5ZlG0Y4Y.d.ts +549 -0
  41. package/dist/media-session-XTP6PP7Q.js +3 -0
  42. package/dist/media-session-XTP6PP7Q.js.map +1 -0
  43. package/dist/note-detection-PPLM7R2H.js +148 -0
  44. package/dist/note-detection-PPLM7R2H.js.map +1 -0
  45. package/dist/sampler-audio-B7MBG3YN.js +3 -0
  46. package/dist/sampler-audio-B7MBG3YN.js.map +1 -0
  47. package/dist/sampler-store-QPHANXYP.js +3 -0
  48. package/dist/sampler-store-QPHANXYP.js.map +1 -0
  49. package/dist/services/track-search-api.d.ts +152 -0
  50. package/dist/services/track-search-api.js +4 -0
  51. package/dist/services/track-search-api.js.map +1 -0
  52. package/dist/store/daw-auth-store.d.ts +31 -0
  53. package/dist/store/daw-auth-store.js +3 -0
  54. package/dist/store/daw-auth-store.js.map +1 -0
  55. package/dist/store/daw-session-store.d.ts +255 -0
  56. package/dist/store/daw-session-store.js +3 -0
  57. package/dist/store/daw-session-store.js.map +1 -0
  58. package/dist/vite/index.d.ts +46 -0
  59. package/dist/vite/index.js +94 -0
  60. package/dist/vite/index.js.map +1 -0
  61. package/dist/workers/analysis-worker.js +379 -0
  62. package/dist/workers/buffer-player-processor-202602.lavv8e32-ts.js +1 -0
  63. package/dist/workers/daw-stem-processor.js +228 -0
  64. package/dist/workers/manifest.json +10 -0
  65. package/dist/workers/phase-vocoder3.js +920 -0
  66. package/dist/workers/realtime-pitch-shift-processor.js +2 -0
  67. package/package.json +151 -0
@@ -0,0 +1,1764 @@
1
+ import { create } from 'zustand';
2
+ import { v4 } from 'uuid';
3
+
4
+ // src/daw/store/daw-session-store.ts
5
+
6
+ // src/daw/interfaces.ts
7
+ var DEFAULT_PITCH_SETTINGS = {
8
+ formantOption: "shifted",
9
+ pitchOption: "highConsistency",
10
+ engineOption: "faster",
11
+ pitchDelayMs: 74
12
+ };
13
+ var stemIdKey = (s) => `${s.trackId}:${s.stemIndex}`;
14
+ function getSelectionBoundsBars(selections) {
15
+ if (selections.length === 0) return null;
16
+ const trackId = selections[0].trackId;
17
+ let startBar = Infinity;
18
+ let endBar = -Infinity;
19
+ const stemIndexes = /* @__PURE__ */ new Set();
20
+ for (const s of selections) {
21
+ if (s.startBar < startBar) startBar = s.startBar;
22
+ if (s.endBar > endBar) endBar = s.endBar;
23
+ stemIndexes.add(s.stemIndex);
24
+ }
25
+ return { trackId, startBar, endBar, stemIndexes: Array.from(stemIndexes).sort((a, b) => a - b) };
26
+ }
27
+ var parseStemIdKey = (key) => {
28
+ const [trackId, idx] = key.split(":");
29
+ const stemIndex = parseInt(idx, 10);
30
+ return trackId && !isNaN(stemIndex) ? { trackId, stemIndex } : null;
31
+ };
32
+ var NOTE_COLORS = [
33
+ "#F5C518",
34
+ // amber
35
+ "#FF6D00",
36
+ // orange
37
+ "#EF4444",
38
+ // red
39
+ "#EC4899",
40
+ // pink
41
+ "#8B5CF6",
42
+ // violet
43
+ "#3B82F6",
44
+ // blue
45
+ "#14B8A6",
46
+ // teal
47
+ "#22C55E"
48
+ // green
49
+ ];
50
+ var NOTE_EMOJI_PALETTE = [
51
+ "\u{1F3AF}",
52
+ "\u{1F496}",
53
+ "\u{1F525}",
54
+ "\u2B50",
55
+ "\u26A1",
56
+ "\u{1F3B5}",
57
+ "\u{1F4A1}",
58
+ "\u2728",
59
+ "\u{1F680}",
60
+ "\u{1F3A4}"
61
+ ];
62
+ function computeClips(ch) {
63
+ const clips = [];
64
+ const sorted = [...ch.muteRegions].sort((a, b) => a.startBar - b.startBar);
65
+ const splits = (ch.splitPoints ?? []).filter((p) => p > ch.trimStartBar && p < ch.trimEndBar).sort((a, b) => a - b);
66
+ let cursor = ch.trimStartBar;
67
+ const pushRange = (lo, hi, leftEdge, rightEdge) => {
68
+ if (hi <= lo) return;
69
+ let segStart = lo;
70
+ let segLeftEdge = leftEdge;
71
+ for (const sp of splits) {
72
+ if (sp <= segStart) continue;
73
+ if (sp >= hi) break;
74
+ clips.push({
75
+ sourceStartBar: segStart,
76
+ sourceEndBar: sp,
77
+ timelineBar: ch.timelineStartBar + segStart,
78
+ leftEdge: segLeftEdge,
79
+ rightEdge: "split"
80
+ });
81
+ segStart = sp;
82
+ segLeftEdge = "split";
83
+ }
84
+ if (segStart < hi) {
85
+ clips.push({
86
+ sourceStartBar: segStart,
87
+ sourceEndBar: hi,
88
+ timelineBar: ch.timelineStartBar + segStart,
89
+ leftEdge: segLeftEdge,
90
+ rightEdge
91
+ });
92
+ }
93
+ };
94
+ for (const mr of sorted) {
95
+ const muteStart = Math.max(mr.startBar, ch.trimStartBar);
96
+ const muteEnd = Math.min(mr.endBar, ch.trimEndBar);
97
+ if (muteStart >= muteEnd) continue;
98
+ if (cursor < muteStart) {
99
+ const leftEdge = cursor === ch.trimStartBar ? "trim" : "mute";
100
+ pushRange(cursor, muteStart, leftEdge, "mute");
101
+ }
102
+ cursor = Math.max(cursor, muteEnd);
103
+ }
104
+ if (cursor < ch.trimEndBar) {
105
+ const leftEdge = cursor === ch.trimStartBar ? "trim" : "mute";
106
+ pushRange(cursor, ch.trimEndBar, leftEdge, "trim");
107
+ }
108
+ return clips;
109
+ }
110
+ var SEGMENT_TYPE = {
111
+ START: 0,
112
+ END: 1,
113
+ INTRO: 2,
114
+ OUTRO: 3,
115
+ BREAK: 4,
116
+ BRIDGE: 5,
117
+ INST: 6,
118
+ SOLO: 7,
119
+ VERSE: 8,
120
+ CHORUS: 9
121
+ };
122
+ var SEGMENT_LABELS = {
123
+ [SEGMENT_TYPE.START]: "Start",
124
+ [SEGMENT_TYPE.END]: "End",
125
+ [SEGMENT_TYPE.INTRO]: "Intro",
126
+ [SEGMENT_TYPE.OUTRO]: "Outro",
127
+ [SEGMENT_TYPE.BREAK]: "Break",
128
+ [SEGMENT_TYPE.BRIDGE]: "Bridge",
129
+ [SEGMENT_TYPE.INST]: "Inst",
130
+ [SEGMENT_TYPE.SOLO]: "Solo",
131
+ [SEGMENT_TYPE.VERSE]: "Verse",
132
+ [SEGMENT_TYPE.CHORUS]: "Chorus"
133
+ };
134
+ var SEGMENT_COLORS = {
135
+ [SEGMENT_TYPE.START]: "#6b7280",
136
+ [SEGMENT_TYPE.END]: "#6b7280",
137
+ [SEGMENT_TYPE.INTRO]: "#FFD600",
138
+ [SEGMENT_TYPE.OUTRO]: "#FFD600",
139
+ [SEGMENT_TYPE.BREAK]: "#f59e0b",
140
+ [SEGMENT_TYPE.BRIDGE]: "#06b6d4",
141
+ [SEGMENT_TYPE.INST]: "#14b8a6",
142
+ [SEGMENT_TYPE.SOLO]: "#f97316",
143
+ [SEGMENT_TYPE.VERSE]: "#3b82f6",
144
+ [SEGMENT_TYPE.CHORUS]: "#ef4444"
145
+ };
146
+ var STEM_LABELS = ["Other", "Vocals", "Bass", "Drums"];
147
+ var STEM_TYPES = ["0_other", "1_vocals", "2_bass", "3_drums"];
148
+ var STEM_COLORS = ["#FF1493", "#FF6D00", "#2979FF", "#FFEA00"];
149
+ var STEM_BUFFER_KEYS = ["other", "vocals", "bass", "drums"];
150
+ var createDefaultChannels = (timelineStartBar = 0, trimStartBar = 0, trimEndBar = 0) => STEM_TYPES.map((stemType, i) => ({
151
+ stemIndex: i,
152
+ stemType,
153
+ label: STEM_LABELS[i],
154
+ volume: 1,
155
+ muted: false,
156
+ soloed: false,
157
+ visible: true,
158
+ timelineStartBar,
159
+ trimStartBar,
160
+ trimEndBar,
161
+ muteRegions: []
162
+ }));
163
+ function segmentInfoToSections(segmentInfo, barMapping) {
164
+ if (!segmentInfo?.length || !barMapping?.length) return [];
165
+ const barTimestamps = barMapping.map((b) => b.sampleStart / 48e3);
166
+ const sections = [];
167
+ for (const [startTime, endTime, segType] of segmentInfo) {
168
+ let startBar = 0;
169
+ let endBar = barMapping.length;
170
+ for (let i = 0; i < barTimestamps.length; i++) {
171
+ if (barTimestamps[i] <= startTime + 0.01) startBar = i;
172
+ }
173
+ for (let i = barTimestamps.length - 1; i >= 0; i--) {
174
+ if (barTimestamps[i] >= endTime - 0.01) {
175
+ endBar = i;
176
+ break;
177
+ }
178
+ }
179
+ if (endBar <= startBar) endBar = startBar + 1;
180
+ sections.push({
181
+ type: segType,
182
+ label: SEGMENT_LABELS[segType] || `S${segType}`,
183
+ startBar,
184
+ endBar
185
+ });
186
+ }
187
+ return sections;
188
+ }
189
+ function findFirstSectionOfType(sections, preferredTypes = [SEGMENT_TYPE.CHORUS, SEGMENT_TYPE.VERSE, SEGMENT_TYPE.BRIDGE]) {
190
+ for (const t of preferredTypes) {
191
+ const found = sections.find((s) => s.type === t);
192
+ if (found) return found;
193
+ }
194
+ return sections.length > 0 ? sections[0] : null;
195
+ }
196
+
197
+ // src/daw/primitives/bar-utils.ts
198
+ var SAMPLE_RATE = 48e3;
199
+ function barToSample(bar, barMapping) {
200
+ if (barMapping.length === 0) return 0;
201
+ const floorBar = Math.floor(bar);
202
+ const frac = bar - floorBar;
203
+ if (floorBar >= barMapping.length) {
204
+ const last = barMapping[barMapping.length - 1];
205
+ return last.sampleStart + last.duration;
206
+ }
207
+ const entry = barMapping[Math.max(0, floorBar)];
208
+ return Math.round(entry.sampleStart + frac * entry.duration);
209
+ }
210
+ var downbeatsToBars = (downbeats) => {
211
+ const bars = [];
212
+ for (let i = 0; i < downbeats.length - 1; i++) {
213
+ const current = downbeats[i];
214
+ const next = downbeats[i + 1];
215
+ bars.push({
216
+ sampleStart: current.frame,
217
+ sampleEnd: next.frame - 1,
218
+ duration: next.frame - current.frame
219
+ });
220
+ }
221
+ return bars;
222
+ };
223
+ var composeBarsFromBeats = (beats) => {
224
+ const downbeats = beats.filter((b) => b.number === 1);
225
+ return downbeatsToBars(downbeats);
226
+ };
227
+ var smoothDownbeats = (downbeats, sampleRate = SAMPLE_RATE, threshold = 2500) => {
228
+ const N = downbeats.length;
229
+ let adjustedDownbeats = [];
230
+ let i = 0;
231
+ while (i < N) {
232
+ const sectionStart = i;
233
+ let sectionAdjustedDownbeats = [];
234
+ let meanInterval = 0;
235
+ sectionAdjustedDownbeats.push({
236
+ frame: downbeats[i].frame,
237
+ number: downbeats[i].number,
238
+ timestamp: downbeats[i].frame / sampleRate
239
+ });
240
+ let j = i + 1;
241
+ while (j <= N) {
242
+ const intervals = downbeats.slice(sectionStart, j).map(
243
+ (_, idx, arr) => idx > 0 ? arr[idx].frame - arr[idx - 1].frame : null
244
+ ).slice(1);
245
+ if (intervals.length > 0) {
246
+ meanInterval = Math.round(
247
+ intervals.filter((i2) => i2 !== null).reduce((a, b) => a + b) / intervals.length
248
+ );
249
+ }
250
+ const adjustedFrames = [];
251
+ for (let idx = 0; idx < j - sectionStart; idx++) {
252
+ adjustedFrames.push(downbeats[sectionStart].frame + meanInterval * idx);
253
+ }
254
+ let deviationsWithinThreshold = true;
255
+ for (let idx = 0; idx < adjustedFrames.length; idx++) {
256
+ const deviation = Math.abs(adjustedFrames[idx] - downbeats[sectionStart + idx].frame);
257
+ if (deviation > threshold) {
258
+ deviationsWithinThreshold = false;
259
+ break;
260
+ }
261
+ }
262
+ if (deviationsWithinThreshold) {
263
+ sectionAdjustedDownbeats = adjustedFrames.map((af, idx) => ({
264
+ frame: af,
265
+ number: downbeats[sectionStart + idx].number,
266
+ timestamp: af / sampleRate
267
+ }));
268
+ j++;
269
+ } else {
270
+ break;
271
+ }
272
+ }
273
+ adjustedDownbeats = adjustedDownbeats.concat(sectionAdjustedDownbeats);
274
+ i = sectionStart + sectionAdjustedDownbeats.length;
275
+ }
276
+ return adjustedDownbeats;
277
+ };
278
+ var composeBarsFromBeatsSmoothed = (beats, sampleRate = SAMPLE_RATE, threshold = 2500) => {
279
+ const downbeats = beats.filter((b) => b.number === 1);
280
+ const smoothed = smoothDownbeats(downbeats, sampleRate, threshold);
281
+ return downbeatsToBars(smoothed);
282
+ };
283
+ var resolveTimeOfDownbeatFromBarIndex = (beats, barIndex) => {
284
+ const beat = beats[barIndex];
285
+ return beat ? beat.timestamp : -1;
286
+ };
287
+ var resolveSampleLocOfDownbeatFromBarIndex = (beats, barIndex) => {
288
+ const beat = beats[barIndex];
289
+ return beat ? Math.floor(beat.timestamp * SAMPLE_RATE) : -1;
290
+ };
291
+ var medianBarDuration = (bars) => {
292
+ if (bars.length === 0) return 0;
293
+ const durations = bars.map((b) => b.duration).sort((a, b) => a - b);
294
+ const mid = Math.floor(durations.length / 2);
295
+ return durations.length % 2 !== 0 ? durations[mid] : (durations[mid - 1] + durations[mid]) / 2;
296
+ };
297
+ var barDurationToBpm = (barDurationSamples) => {
298
+ if (barDurationSamples <= 0) return 0;
299
+ const barDurationSeconds = barDurationSamples / SAMPLE_RATE;
300
+ const beatsPerBar = 4;
301
+ return beatsPerBar * 60 / barDurationSeconds;
302
+ };
303
+ var bpmToBarDuration = (bpm) => {
304
+ if (bpm <= 0) return 0;
305
+ const beatsPerBar = 4;
306
+ const barDurationSeconds = beatsPerBar * 60 / bpm;
307
+ return Math.round(barDurationSeconds * SAMPLE_RATE);
308
+ };
309
+
310
+ // src/daw/primitives/tempo-utils.ts
311
+ var calculateTempoMatch = (masterBarLength, trackBarLength) => {
312
+ const possibleLengths = [
313
+ [masterBarLength / 2, 2],
314
+ [masterBarLength, 1],
315
+ [masterBarLength * 2, 0.5]
316
+ ];
317
+ let bestTarget = possibleLengths[1];
318
+ let bestDiff = Math.abs(masterBarLength - trackBarLength);
319
+ for (const candidate of possibleLengths) {
320
+ const diff = Math.abs(candidate[0] - trackBarLength);
321
+ if (diff < bestDiff) {
322
+ bestDiff = diff;
323
+ bestTarget = candidate;
324
+ }
325
+ }
326
+ return {
327
+ targetBarLength: bestTarget[0],
328
+ playbackRate: trackBarLength / bestTarget[0],
329
+ ratio: bestTarget[1]
330
+ };
331
+ };
332
+ var computeBarSpeedRatio = (barDurationSamples, targetBarLengthSamples, ratio) => {
333
+ return barDurationSamples / ratio / targetBarLengthSamples;
334
+ };
335
+ var playbackRateToSemitones = (playbackRate) => {
336
+ return 12 * Math.log2(playbackRate);
337
+ };
338
+ var semitonesToPitchFactor = (semitones) => {
339
+ return Math.pow(2, semitones / 12);
340
+ };
341
+ var keyToSemitones = {
342
+ A: 1,
343
+ "A#": 2,
344
+ B: 3,
345
+ C: 4,
346
+ "C#": 5,
347
+ D: 6,
348
+ "D#": 7,
349
+ E: 8,
350
+ F: 9,
351
+ "F#": 10,
352
+ G: 11,
353
+ "G#": 12
354
+ };
355
+ var semitonesToKey = Object.entries(keyToSemitones).reduce(
356
+ (acc, [key, value]) => {
357
+ acc[value] = key;
358
+ return acc;
359
+ },
360
+ {}
361
+ );
362
+ var convertKeyToRelativeMinor = (key, tonality) => {
363
+ let semitones = keyToSemitones[key];
364
+ if (tonality === "major") {
365
+ semitones = (semitones - 3 + 12) % 12 || 12;
366
+ }
367
+ return semitones;
368
+ };
369
+ var PitchShiftStrategy = /* @__PURE__ */ ((PitchShiftStrategy2) => {
370
+ PitchShiftStrategy2["DEFAULT"] = "DEFAULT";
371
+ PitchShiftStrategy2["PRESERVE_MIX_KEY"] = "PRESERVE_MIX_KEY";
372
+ PitchShiftStrategy2["MATCH_SEED_KEY"] = "MATCH_SEED_KEY";
373
+ PitchShiftStrategy2["NO_SHIFT"] = "NO_SHIFT";
374
+ PitchShiftStrategy2["KEY_PRESERVE"] = "KEY_PRESERVE";
375
+ return PitchShiftStrategy2;
376
+ })(PitchShiftStrategy || {});
377
+ var computePitchShift = (refKey, refTonality, refBarLength, trackKey, trackTonality, trackBarLength, strategy = "KEY_PRESERVE" /* KEY_PRESERVE */) => {
378
+ if (!refBarLength || !trackBarLength || isNaN(refBarLength) || isNaN(trackBarLength)) return 0;
379
+ const tempoMatch = calculateTempoMatch(refBarLength, trackBarLength);
380
+ const semitoneChangeFromTempo = playbackRateToSemitones(tempoMatch.playbackRate);
381
+ if (strategy === "NO_SHIFT" /* NO_SHIFT */) return 0;
382
+ if (strategy === "KEY_PRESERVE" /* KEY_PRESERVE */) return -semitoneChangeFromTempo;
383
+ const seedKeySemitones = convertKeyToRelativeMinor(refKey, refTonality);
384
+ const mixKeySemitones = convertKeyToRelativeMinor(trackKey, trackTonality);
385
+ if (strategy === "MATCH_SEED_KEY" /* MATCH_SEED_KEY */) {
386
+ let shift = seedKeySemitones - mixKeySemitones;
387
+ if (shift > 6) shift -= 12;
388
+ if (shift < -6) shift += 12;
389
+ return -semitoneChangeFromTempo + shift;
390
+ }
391
+ const adjustedMix = (mixKeySemitones + semitoneChangeFromTempo + 12) % 12 || 12;
392
+ const possibleMatches = [
393
+ seedKeySemitones,
394
+ (seedKeySemitones + 5) % 12 || 12,
395
+ (seedKeySemitones - 5 + 12) % 12 || 12
396
+ ];
397
+ const shifts = possibleMatches.map((match) => {
398
+ let s = match - adjustedMix;
399
+ if (s > 6) s -= 12;
400
+ if (s < -6) s += 12;
401
+ const totalChange = Math.abs(s + semitoneChangeFromTempo);
402
+ return {
403
+ shift: s,
404
+ weightedChange: match === seedKeySemitones ? totalChange * 1 : totalChange
405
+ };
406
+ });
407
+ const best = shifts.reduce(
408
+ (prev, curr) => curr.weightedChange < prev.weightedChange ? curr : prev
409
+ );
410
+ return best.shift;
411
+ };
412
+ var CAMELOT_MAP = {
413
+ "Ab_minor": { number: 1, letter: "A", label: "1A" },
414
+ "B_major": { number: 1, letter: "B", label: "1B" },
415
+ "Eb_minor": { number: 2, letter: "A", label: "2A" },
416
+ "F#_major": { number: 2, letter: "B", label: "2B" },
417
+ "Bb_minor": { number: 3, letter: "A", label: "3A" },
418
+ "Db_major": { number: 3, letter: "B", label: "3B" },
419
+ "F_minor": { number: 4, letter: "A", label: "4A" },
420
+ "Ab_major": { number: 4, letter: "B", label: "4B" },
421
+ "C_minor": { number: 5, letter: "A", label: "5A" },
422
+ "Eb_major": { number: 5, letter: "B", label: "5B" },
423
+ "G_minor": { number: 6, letter: "A", label: "6A" },
424
+ "Bb_major": { number: 6, letter: "B", label: "6B" },
425
+ "D_minor": { number: 7, letter: "A", label: "7A" },
426
+ "F_major": { number: 7, letter: "B", label: "7B" },
427
+ "A_minor": { number: 8, letter: "A", label: "8A" },
428
+ "C_major": { number: 8, letter: "B", label: "8B" },
429
+ "E_minor": { number: 9, letter: "A", label: "9A" },
430
+ "G_major": { number: 9, letter: "B", label: "9B" },
431
+ "B_minor": { number: 10, letter: "A", label: "10A" },
432
+ "D_major": { number: 10, letter: "B", label: "10B" },
433
+ "F#_minor": { number: 11, letter: "A", label: "11A" },
434
+ "A_major": { number: 11, letter: "B", label: "11B" },
435
+ "Db_minor": { number: 12, letter: "A", label: "12A" },
436
+ "E_major": { number: 12, letter: "B", label: "12B" }
437
+ };
438
+ var SHARP_TO_FLAT = {
439
+ "A#": "Bb",
440
+ "C#": "Db",
441
+ "D#": "Eb",
442
+ "F#": "F#",
443
+ "G#": "Ab"
444
+ };
445
+ var normaliseKeyForCamelot = (key) => {
446
+ return SHARP_TO_FLAT[key] ?? key;
447
+ };
448
+ var toCamelot = (key, tonality) => {
449
+ const normKey = normaliseKeyForCamelot(key);
450
+ const lookup = `${normKey}_${tonality}`;
451
+ return CAMELOT_MAP[lookup] ?? null;
452
+ };
453
+ var REVERSE_CAMELOT = {};
454
+ for (const [k, v] of Object.entries(CAMELOT_MAP)) {
455
+ const [rawKey, tonality] = k.split("_");
456
+ const flatToSharp = {
457
+ "Bb": "A#",
458
+ "Db": "C#",
459
+ "Eb": "D#",
460
+ "Ab": "G#"
461
+ };
462
+ const musicKey = flatToSharp[rawKey] ?? rawKey;
463
+ REVERSE_CAMELOT[v.label] = { key: musicKey, tonality };
464
+ }
465
+ var camelotMatchType = (a, b) => {
466
+ const numDiff = Math.abs(a.number - b.number);
467
+ const wrappedDiff = numDiff === 11 ? 1 : numDiff;
468
+ if (a.number === b.number && a.letter === b.letter) return "exact";
469
+ if (a.number === b.number && a.letter !== b.letter) return "relative";
470
+ if (a.letter === b.letter && wrappedDiff === 1) return "adjacent";
471
+ if (a.letter !== b.letter && wrappedDiff === 1) return "diagonal";
472
+ return "none";
473
+ };
474
+ var transposeKey = (key, tonality, semitones) => {
475
+ const base = keyToSemitones[key];
476
+ const shifted = (base - 1 + semitones % 12 + 12) % 12 + 1;
477
+ return { key: semitonesToKey[shifted], tonality };
478
+ };
479
+ var computeTransposeSuggestions = (trackKey, trackTonality, otherTrackKeys) => {
480
+ if (otherTrackKeys.length === 0) return [];
481
+ const otherCamelots = otherTrackKeys.map((t) => toCamelot(t.key, t.tonality)).filter((c) => c !== null);
482
+ if (otherCamelots.length === 0) return [];
483
+ const refCamelot = otherCamelots[0];
484
+ const suggestions = [];
485
+ for (let st = -12; st <= 12; st++) {
486
+ const { key: rk, tonality: rt } = transposeKey(trackKey, trackTonality, st);
487
+ const rc = toCamelot(rk, rt);
488
+ if (!rc) continue;
489
+ const matchType = camelotMatchType(refCamelot, rc);
490
+ if (matchType === "none") continue;
491
+ const keyStr = `${rk}${rt === "minor" ? "m" : ""}`;
492
+ suggestions.push({
493
+ semitones: st,
494
+ resultKey: rk,
495
+ resultTonality: rt,
496
+ resultCamelot: rc,
497
+ matchType,
498
+ label: `${st > 0 ? "+" : ""}${st} \u2192 ${keyStr} (${rc.label})`
499
+ });
500
+ }
501
+ const order = { exact: 0, adjacent: 1, relative: 2, diagonal: 3, none: 4 };
502
+ suggestions.sort((a, b) => {
503
+ const od = order[a.matchType] - order[b.matchType];
504
+ if (od !== 0) return od;
505
+ return Math.abs(a.semitones) - Math.abs(b.semitones);
506
+ });
507
+ return suggestions;
508
+ };
509
+
510
+ // src/daw/engine/daw-cache.ts
511
+ var SESSION_KEY = "daw-session-v1";
512
+ async function deriveKey(password, salt) {
513
+ const enc = new TextEncoder();
514
+ const keyMaterial = await crypto.subtle.importKey(
515
+ "raw",
516
+ enc.encode(password),
517
+ "PBKDF2",
518
+ false,
519
+ ["deriveKey"]
520
+ );
521
+ return crypto.subtle.deriveKey(
522
+ { name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" },
523
+ keyMaterial,
524
+ { name: "AES-GCM", length: 256 },
525
+ false,
526
+ ["encrypt", "decrypt"]
527
+ );
528
+ }
529
+ function bufToBase64(buf) {
530
+ return btoa(String.fromCharCode(...new Uint8Array(buf)));
531
+ }
532
+ function base64ToBuf(b64) {
533
+ const bin = atob(b64);
534
+ const buf = new Uint8Array(bin.length);
535
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
536
+ return buf;
537
+ }
538
+ async function encryptPayload(json, password) {
539
+ const salt = crypto.getRandomValues(new Uint8Array(16));
540
+ const iv = crypto.getRandomValues(new Uint8Array(12));
541
+ const key = await deriveKey(password, salt);
542
+ const ciphertext = await crypto.subtle.encrypt(
543
+ { name: "AES-GCM", iv },
544
+ key,
545
+ new TextEncoder().encode(json)
546
+ );
547
+ return {
548
+ salt: bufToBase64(salt),
549
+ iv: bufToBase64(iv),
550
+ ciphertext: bufToBase64(ciphertext)
551
+ };
552
+ }
553
+ async function decryptPayload(enc, password) {
554
+ const salt = base64ToBuf(enc.salt);
555
+ const iv = base64ToBuf(enc.iv);
556
+ const ciphertext = base64ToBuf(enc.ciphertext);
557
+ const key = await deriveKey(password, salt);
558
+ const plainBuf = await crypto.subtle.decrypt(
559
+ { name: "AES-GCM", iv },
560
+ key,
561
+ ciphertext
562
+ );
563
+ return new TextDecoder().decode(plainBuf);
564
+ }
565
+ function saveSession(session) {
566
+ try {
567
+ localStorage.setItem(SESSION_KEY, JSON.stringify(session));
568
+ } catch {
569
+ }
570
+ }
571
+ function loadSession() {
572
+ try {
573
+ const raw = localStorage.getItem(SESSION_KEY);
574
+ if (!raw) return null;
575
+ return JSON.parse(raw);
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ function clearSession() {
581
+ try {
582
+ localStorage.removeItem(SESSION_KEY);
583
+ } catch {
584
+ }
585
+ }
586
+ var DB_NAME = "daw-audio-cache";
587
+ var DB_VERSION = 1;
588
+ var STORE_NAME = "mp4-buffers";
589
+ function openDB() {
590
+ return new Promise((resolve, reject) => {
591
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
592
+ req.onupgradeneeded = () => {
593
+ const db = req.result;
594
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
595
+ db.createObjectStore(STORE_NAME);
596
+ }
597
+ };
598
+ req.onsuccess = () => resolve(req.result);
599
+ req.onerror = () => reject(req.error);
600
+ });
601
+ }
602
+ async function cacheAudioBuffer(url, data) {
603
+ try {
604
+ const db = await openDB();
605
+ const tx = db.transaction(STORE_NAME, "readwrite");
606
+ tx.objectStore(STORE_NAME).put(data, url);
607
+ await new Promise((res, rej) => {
608
+ tx.oncomplete = () => res();
609
+ tx.onerror = () => rej(tx.error);
610
+ });
611
+ } catch {
612
+ }
613
+ }
614
+ async function getCachedAudioBuffer(url) {
615
+ try {
616
+ const db = await openDB();
617
+ const tx = db.transaction(STORE_NAME, "readonly");
618
+ const req = tx.objectStore(STORE_NAME).get(url);
619
+ return new Promise((resolve) => {
620
+ req.onsuccess = () => resolve(req.result ?? null);
621
+ req.onerror = () => resolve(null);
622
+ });
623
+ } catch {
624
+ return null;
625
+ }
626
+ }
627
+ async function clearAudioCache() {
628
+ try {
629
+ const db = await openDB();
630
+ const tx = db.transaction(STORE_NAME, "readwrite");
631
+ tx.objectStore(STORE_NAME).clear();
632
+ await new Promise((res) => {
633
+ tx.oncomplete = () => res();
634
+ });
635
+ } catch {
636
+ }
637
+ }
638
+ async function clearAll() {
639
+ clearSession();
640
+ await clearAudioCache();
641
+ }
642
+ var PROJECT_FILE_VERSION = 1;
643
+ function buildPersistedSession(s) {
644
+ return {
645
+ id: s.id,
646
+ mixName: s.mixName,
647
+ masterTempo: s.masterTempo,
648
+ originalTempo: s.originalTempo,
649
+ masterBarLength48000: s.masterBarLength48000,
650
+ timeSignature: s.timeSignature,
651
+ globalPlaybackRate: s.globalPlaybackRate,
652
+ playheadBar: s.playheadBar,
653
+ loopRegion: s.loopRegion,
654
+ notes: s.notes?.map((n) => ({ ...n })) ?? [],
655
+ tracks: s.tracks.map((t) => ({
656
+ trackDataJson: JSON.stringify(t.trackData),
657
+ channels: t.channels.map((ch) => ({
658
+ stemIndex: ch.stemIndex,
659
+ volume: ch.volume,
660
+ muted: ch.muted,
661
+ soloed: ch.soloed,
662
+ visible: ch.visible,
663
+ timelineStartBar: ch.timelineStartBar,
664
+ trimStartBar: ch.trimStartBar,
665
+ trimEndBar: ch.trimEndBar,
666
+ muteRegions: ch.muteRegions.map((r) => ({ startBar: r.startBar, endBar: r.endBar })),
667
+ resampledRegions: ch.resampledRegions?.map((r) => ({
668
+ startBar: r.startBar,
669
+ endBar: r.endBar,
670
+ sliceCount: r.sliceCount
671
+ })),
672
+ effects: ch.effects ? JSON.parse(JSON.stringify(ch.effects)) : void 0
673
+ })),
674
+ volume: t.volume,
675
+ muted: t.muted,
676
+ soloed: t.soloed,
677
+ expanded: t.expanded,
678
+ color: t.color,
679
+ artwork: t.artwork,
680
+ displayName: t.displayName,
681
+ artistName: t.artistName,
682
+ transposeSemitones: t.transposeSemitones ?? 0,
683
+ pitchSettings: t.pitchSettings,
684
+ perBarPitchCompensation: t.perBarPitchCompensation === false ? false : t.perBarPitchCompensation ?? true,
685
+ beatGridOffsetBeats: t.beatGridOffsetBeats ?? 0
686
+ })),
687
+ // Pending tracks are already in PersistedTrack shape; pass
688
+ // them through verbatim so the deferred-load queue survives a
689
+ // refresh. We deep-clone to detach from the store's reference
690
+ // (matches the rest of this function's "snapshot, not view"
691
+ // contract).
692
+ pendingTracks: s.pendingTracks && s.pendingTracks.length > 0 ? s.pendingTracks.map((pt) => ({ ...pt })) : void 0
693
+ };
694
+ }
695
+ async function exportProject(session, password) {
696
+ const now = (/* @__PURE__ */ new Date()).toISOString();
697
+ let project;
698
+ if (password && password.length > 0) {
699
+ const innerProject = {
700
+ version: PROJECT_FILE_VERSION,
701
+ createdAt: now,
702
+ updatedAt: now,
703
+ session
704
+ };
705
+ const enc = await encryptPayload(JSON.stringify(innerProject), password);
706
+ project = {
707
+ version: PROJECT_FILE_VERSION,
708
+ createdAt: now,
709
+ updatedAt: now,
710
+ session: void 0,
711
+ encrypted: enc
712
+ };
713
+ } else {
714
+ project = {
715
+ version: PROJECT_FILE_VERSION,
716
+ createdAt: now,
717
+ updatedAt: now,
718
+ session
719
+ };
720
+ }
721
+ const json = JSON.stringify(project, null, 2);
722
+ const blob = new Blob([json], { type: "application/json" });
723
+ const filename = `${session.mixName || "untitled"}.stem-project`;
724
+ const url = URL.createObjectURL(blob);
725
+ const a = document.createElement("a");
726
+ a.href = url;
727
+ a.download = filename;
728
+ document.body.appendChild(a);
729
+ a.click();
730
+ document.body.removeChild(a);
731
+ URL.revokeObjectURL(url);
732
+ }
733
+ var PROJECT_DB_NAME = "daw-project-library";
734
+ var PROJECT_DB_VERSION = 1;
735
+ var PROJECT_STORE = "projects";
736
+ function openProjectDB() {
737
+ return new Promise((resolve, reject) => {
738
+ const req = indexedDB.open(PROJECT_DB_NAME, PROJECT_DB_VERSION);
739
+ req.onupgradeneeded = () => {
740
+ const db = req.result;
741
+ if (!db.objectStoreNames.contains(PROJECT_STORE)) {
742
+ db.createObjectStore(PROJECT_STORE, { keyPath: "meta.id" });
743
+ }
744
+ };
745
+ req.onsuccess = () => resolve(req.result);
746
+ req.onerror = () => reject(req.error);
747
+ });
748
+ }
749
+ async function saveProjectToLibrary(session) {
750
+ const now = (/* @__PURE__ */ new Date()).toISOString();
751
+ const project = {
752
+ version: PROJECT_FILE_VERSION,
753
+ createdAt: now,
754
+ updatedAt: now,
755
+ session
756
+ };
757
+ const artworks = session.tracks.map((t) => t.artwork).filter((a) => !!a && a.length > 0).slice(0, 4);
758
+ const meta = {
759
+ id: session.id,
760
+ name: session.mixName || "Untitled Mix",
761
+ createdAt: now,
762
+ updatedAt: now,
763
+ trackCount: session.tracks.length,
764
+ masterTempo: Math.round(session.masterTempo),
765
+ artworks
766
+ };
767
+ const stored = { meta, data: project };
768
+ try {
769
+ const db = await openProjectDB();
770
+ const existing = await new Promise((resolve) => {
771
+ const tx2 = db.transaction(PROJECT_STORE, "readonly");
772
+ const req = tx2.objectStore(PROJECT_STORE).get(session.id);
773
+ req.onsuccess = () => resolve(req.result);
774
+ req.onerror = () => resolve(void 0);
775
+ });
776
+ if (existing) {
777
+ meta.createdAt = existing.meta.createdAt;
778
+ stored.data.createdAt = existing.data.createdAt;
779
+ }
780
+ const tx = db.transaction(PROJECT_STORE, "readwrite");
781
+ tx.objectStore(PROJECT_STORE).put(stored);
782
+ await new Promise((res, rej) => {
783
+ tx.oncomplete = () => res();
784
+ tx.onerror = () => rej(tx.error);
785
+ });
786
+ } catch (err) {
787
+ console.warn("[ProjectLibrary] Failed to save:", err);
788
+ }
789
+ return meta;
790
+ }
791
+ async function listProjects() {
792
+ try {
793
+ const db = await openProjectDB();
794
+ const tx = db.transaction(PROJECT_STORE, "readonly");
795
+ const store = tx.objectStore(PROJECT_STORE);
796
+ const all = await new Promise((resolve) => {
797
+ const req = store.getAll();
798
+ req.onsuccess = () => resolve(req.result);
799
+ req.onerror = () => resolve([]);
800
+ });
801
+ return all.map((s) => s.meta).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
802
+ } catch {
803
+ return [];
804
+ }
805
+ }
806
+ async function loadProjectFromLibrary(id) {
807
+ try {
808
+ const db = await openProjectDB();
809
+ const tx = db.transaction(PROJECT_STORE, "readonly");
810
+ const req = tx.objectStore(PROJECT_STORE).get(id);
811
+ return new Promise((resolve) => {
812
+ req.onsuccess = () => {
813
+ const stored = req.result;
814
+ resolve(stored?.data ?? null);
815
+ };
816
+ req.onerror = () => resolve(null);
817
+ });
818
+ } catch {
819
+ return null;
820
+ }
821
+ }
822
+ async function deleteProjectFromLibrary(id) {
823
+ try {
824
+ const db = await openProjectDB();
825
+ const tx = db.transaction(PROJECT_STORE, "readwrite");
826
+ tx.objectStore(PROJECT_STORE).delete(id);
827
+ await new Promise((res) => {
828
+ tx.oncomplete = () => res();
829
+ });
830
+ } catch {
831
+ }
832
+ }
833
+ function isProjectEncrypted(file) {
834
+ return !!(file.encrypted && typeof file.encrypted === "object");
835
+ }
836
+ async function importProject(file, password) {
837
+ const text = await file.text();
838
+ let parsed;
839
+ try {
840
+ parsed = JSON.parse(text);
841
+ } catch {
842
+ throw new Error("Invalid project file: not valid JSON");
843
+ }
844
+ let obj = parsed;
845
+ if (typeof obj !== "object" || obj === null) {
846
+ throw new Error("Invalid project file: expected a JSON object");
847
+ }
848
+ if (obj.version !== PROJECT_FILE_VERSION) {
849
+ throw new Error(`Unsupported project version: ${obj.version} (expected ${PROJECT_FILE_VERSION})`);
850
+ }
851
+ if (isProjectEncrypted(obj)) {
852
+ if (!password) {
853
+ throw new Error("PASSWORD_REQUIRED");
854
+ }
855
+ const enc = obj.encrypted;
856
+ let decryptedJson;
857
+ try {
858
+ decryptedJson = await decryptPayload(enc, password);
859
+ } catch {
860
+ throw new Error("WRONG_PASSWORD");
861
+ }
862
+ let inner;
863
+ try {
864
+ inner = JSON.parse(decryptedJson);
865
+ } catch {
866
+ throw new Error("WRONG_PASSWORD");
867
+ }
868
+ obj = inner;
869
+ }
870
+ const session = obj.session;
871
+ if (!session || typeof session !== "object") {
872
+ throw new Error("Invalid project file: missing session data");
873
+ }
874
+ if (!Array.isArray(session.tracks) || session.tracks.length === 0) {
875
+ throw new Error("Invalid project file: session has no tracks");
876
+ }
877
+ for (const t of session.tracks) {
878
+ if (!t.trackDataJson || typeof t.trackDataJson !== "string") {
879
+ throw new Error("Invalid project file: track missing trackDataJson");
880
+ }
881
+ try {
882
+ JSON.parse(t.trackDataJson);
883
+ } catch {
884
+ throw new Error("Invalid project file: track has corrupt trackDataJson");
885
+ }
886
+ }
887
+ return obj;
888
+ }
889
+
890
+ // src/daw/store/daw-session-store.ts
891
+ var MAX_UNDO = 60;
892
+ function captureSnapshot(state) {
893
+ return {
894
+ tracks: JSON.parse(JSON.stringify(state.tracks)),
895
+ masterTempo: state.masterTempo,
896
+ originalTempo: state.originalTempo,
897
+ masterBarLength48000: state.masterBarLength48000,
898
+ loopRegion: state.loopRegion ? { ...state.loopRegion } : null,
899
+ mixName: state.mixName,
900
+ notes: state.notes.map((n) => ({ ...n }))
901
+ };
902
+ }
903
+ var _saveTimer = null;
904
+ var _librarySaveTimer = null;
905
+ var SAVE_DEBOUNCE_MS = 1500;
906
+ var LIBRARY_SAVE_DEBOUNCE_MS = 1e4;
907
+ function schedulePersist(_state) {
908
+ if (_saveTimer) clearTimeout(_saveTimer);
909
+ _saveTimer = setTimeout(() => {
910
+ saveSession(buildPersistedSession(useDAWSessionStore.getState()));
911
+ }, SAVE_DEBOUNCE_MS);
912
+ if (_librarySaveTimer) clearTimeout(_librarySaveTimer);
913
+ _librarySaveTimer = setTimeout(() => {
914
+ const s = useDAWSessionStore.getState();
915
+ if (s.tracks.length > 0) {
916
+ saveProjectToLibrary(buildPersistedSession(s));
917
+ }
918
+ }, LIBRARY_SAVE_DEBOUNCE_MS);
919
+ }
920
+ var SAMPLE_RATE2 = 48e3;
921
+ function composeBarsWithOffset(beats, offsetBeats) {
922
+ if (offsetBeats === 0) return composeBarsFromBeats(beats);
923
+ if (beats.length < 2) return composeBarsFromBeats(beats);
924
+ const intervals = [];
925
+ for (let i = 1; i < beats.length; i++) {
926
+ intervals.push(beats[i].frame - beats[i - 1].frame);
927
+ }
928
+ intervals.sort((a, b) => a - b);
929
+ const medianInterval = intervals[Math.floor(intervals.length / 2)] || 0;
930
+ const wholeBeatOffset = Math.floor(offsetBeats);
931
+ const fracBeatOffset = offsetBeats - wholeBeatOffset;
932
+ const frameFracOffset = Math.round(fracBeatOffset * medianInterval);
933
+ const offsetBeatsList = [];
934
+ for (let i = 0; i < beats.length; i++) {
935
+ const origBeat = beats[i];
936
+ const newFrame = origBeat.frame + frameFracOffset;
937
+ if (newFrame < 0) continue;
938
+ const shifted = ((origBeat.number - 1 - wholeBeatOffset) % 4 + 4) % 4 + 1;
939
+ offsetBeatsList.push({
940
+ frame: newFrame,
941
+ number: shifted,
942
+ timestamp: newFrame / SAMPLE_RATE2
943
+ });
944
+ }
945
+ const result = composeBarsFromBeats(offsetBeatsList);
946
+ if (result.length < 2) return composeBarsFromBeats(beats);
947
+ return result;
948
+ }
949
+ function mergeMuteRegions(regions) {
950
+ if (regions.length <= 1) return regions;
951
+ const sorted = [...regions].sort((a, b) => a.startBar - b.startBar);
952
+ const merged = [sorted[0]];
953
+ for (let i = 1; i < sorted.length; i++) {
954
+ const last = merged[merged.length - 1];
955
+ if (sorted[i].startBar <= last.endBar) {
956
+ last.endBar = Math.max(last.endBar, sorted[i].endBar);
957
+ } else {
958
+ merged.push(sorted[i]);
959
+ }
960
+ }
961
+ return merged;
962
+ }
963
+ var recalculateSpeedRatios = (tracks, masterBarLength) => tracks.map((track) => {
964
+ const tempoMatch = calculateTempoMatch(masterBarLength, track.nativeBarLength);
965
+ return {
966
+ ...track,
967
+ speedRatioToMaster: tempoMatch.playbackRate,
968
+ tempoRatio: tempoMatch.ratio
969
+ };
970
+ });
971
+ function computeSmartAlignment(existingTracks, newBarMapping, newSegmentInfo) {
972
+ if (existingTracks.length === 0) return 0;
973
+ const newSections = segmentInfoToSections(newSegmentInfo, newBarMapping);
974
+ const newAnchor = findFirstSectionOfType(newSections, [
975
+ SEGMENT_TYPE.CHORUS,
976
+ SEGMENT_TYPE.VERSE,
977
+ SEGMENT_TYPE.BRIDGE,
978
+ SEGMENT_TYPE.INTRO
979
+ ]);
980
+ if (!newAnchor) return 0;
981
+ const refTrack = existingTracks[0];
982
+ const refSections = segmentInfoToSections(refTrack.segmentInfo, refTrack.barMapping);
983
+ const matchTypes = [newAnchor.type, SEGMENT_TYPE.CHORUS, SEGMENT_TYPE.VERSE];
984
+ let refAnchor = null;
985
+ for (const t of matchTypes) {
986
+ refAnchor = refSections.find((s) => s.type === t);
987
+ if (refAnchor) break;
988
+ }
989
+ if (!refAnchor) refAnchor = findFirstSectionOfType(refSections);
990
+ if (!refAnchor) return 0;
991
+ const refChannel = refTrack.channels.find((ch) => ch.visible) || refTrack.channels[0];
992
+ const refAnchorTimeline = refChannel.timelineStartBar + refAnchor.startBar;
993
+ const offset = refAnchorTimeline - newAnchor.startBar;
994
+ return Math.max(0, Math.round(offset));
995
+ }
996
+ var useDAWSessionStore = create((set, get) => ({
997
+ // ── Initial state ──
998
+ id: v4(),
999
+ mixName: "Untitled Mix",
1000
+ masterTempo: 0,
1001
+ originalTempo: 0,
1002
+ masterBarLength48000: 0,
1003
+ timeSignature: [4, 4],
1004
+ tracks: [],
1005
+ playheadBar: 0,
1006
+ isPlaying: false,
1007
+ globalPlaybackRate: 1,
1008
+ loopRegion: null,
1009
+ barSubdivisions: 1,
1010
+ notes: [],
1011
+ pendingTracks: [],
1012
+ toolMode: "pointer",
1013
+ compositeView: false,
1014
+ similarTracksBalance: 0.5,
1015
+ similarTracksPopularity: 1,
1016
+ selectedStem: null,
1017
+ selections: [],
1018
+ selectionRange: null,
1019
+ clipboardEntries: [],
1020
+ clipboard: null,
1021
+ _undoStack: [],
1022
+ _redoStack: [],
1023
+ // ──────────────────────────────────────────────────────────────────
1024
+ // Track management
1025
+ // ──────────────────────────────────────────────────────────────────
1026
+ addTrack: (trackData) => {
1027
+ get().pushUndo();
1028
+ const state = get();
1029
+ const barMapping = composeBarsFromBeats(trackData.beats);
1030
+ const nativeBarLength = trackData.tempoOrigSz || medianBarDuration(barMapping);
1031
+ const nativeBpm = trackData.bpm || barDurationToBpm(nativeBarLength);
1032
+ const isFirstTrack = state.tracks.length === 0;
1033
+ const masterBarLength = isFirstTrack ? nativeBarLength : state.masterBarLength48000;
1034
+ const masterTempo = isFirstTrack ? nativeBpm : state.masterTempo;
1035
+ const tempoMatch = calculateTempoMatch(masterBarLength, nativeBarLength);
1036
+ const pitchCorrection = computePitchShift(
1037
+ state.tracks[0]?.key ?? trackData.key,
1038
+ state.tracks[0]?.tonality ?? trackData.tonality,
1039
+ masterBarLength,
1040
+ trackData.key,
1041
+ trackData.tonality,
1042
+ nativeBarLength,
1043
+ "KEY_PRESERVE" /* KEY_PRESERVE */
1044
+ );
1045
+ const rawColor = trackData.colors?.[0] || trackData.artworkGlowColor || trackData.color1Hex;
1046
+ const resolvedColor = rawColor ? rawColor.startsWith("#") ? rawColor : `#${rawColor}` : "#5b8def";
1047
+ const segmentInfo = trackData.segmentInfo ?? [];
1048
+ const timelineStart = computeSmartAlignment(state.tracks, barMapping, segmentInfo);
1049
+ const trimEnd = barMapping.length - 1;
1050
+ const newTrack = {
1051
+ id: v4(),
1052
+ trackData,
1053
+ audioSourceSequenceId: trackData.trackId,
1054
+ barMapping,
1055
+ segmentInfo,
1056
+ nativeBarLength,
1057
+ nativeBpm,
1058
+ speedRatioToMaster: tempoMatch.playbackRate,
1059
+ tempoRatio: tempoMatch.ratio,
1060
+ pitchCorrectionSemitones: pitchCorrection,
1061
+ key: trackData.key,
1062
+ tonality: trackData.tonality,
1063
+ beatGridOffsetBeats: 0,
1064
+ transposeSemitones: 0,
1065
+ pitchSettings: { ...DEFAULT_PITCH_SETTINGS },
1066
+ perBarPitchCompensation: true,
1067
+ channels: createDefaultChannels(timelineStart, 0, trimEnd),
1068
+ volume: 1,
1069
+ muted: false,
1070
+ soloed: false,
1071
+ color: resolvedColor,
1072
+ artwork: trackData.artwork || "",
1073
+ displayName: trackData.name || trackData.trackDisplayName || "Untitled",
1074
+ artistName: trackData.artistDisplayName || "Unknown",
1075
+ expanded: false
1076
+ };
1077
+ const updatedTracks = [...state.tracks, newTrack];
1078
+ if (isFirstTrack) {
1079
+ set({
1080
+ masterTempo,
1081
+ originalTempo: masterTempo,
1082
+ masterBarLength48000: masterBarLength,
1083
+ tracks: updatedTracks
1084
+ });
1085
+ } else {
1086
+ set({ tracks: recalculateSpeedRatios(updatedTracks, masterBarLength) });
1087
+ }
1088
+ schedulePersist(get());
1089
+ return newTrack;
1090
+ },
1091
+ removeTrack: (trackId) => {
1092
+ get().pushUndo();
1093
+ const state = get();
1094
+ const filtered = state.tracks.filter((t) => t.id !== trackId);
1095
+ if (filtered.length === 0) {
1096
+ set({ tracks: [], masterTempo: 0, originalTempo: 0, masterBarLength48000: 0 });
1097
+ schedulePersist(get());
1098
+ return;
1099
+ }
1100
+ if (state.tracks[0]?.id === trackId) {
1101
+ const newMaster = filtered[0];
1102
+ const masterBarLength = newMaster.nativeBarLength;
1103
+ const masterTempo = newMaster.nativeBpm;
1104
+ set({
1105
+ masterTempo,
1106
+ originalTempo: masterTempo,
1107
+ masterBarLength48000: masterBarLength,
1108
+ tracks: recalculateSpeedRatios(filtered, masterBarLength)
1109
+ });
1110
+ } else {
1111
+ set({ tracks: filtered });
1112
+ }
1113
+ schedulePersist(get());
1114
+ },
1115
+ reorderTrack: (trackId, newIndex) => {
1116
+ get().pushUndo();
1117
+ const state = get();
1118
+ const tracks = [...state.tracks];
1119
+ const currentIdx = tracks.findIndex((t) => t.id === trackId);
1120
+ if (currentIdx === -1) return;
1121
+ const [track] = tracks.splice(currentIdx, 1);
1122
+ const clampedIndex = Math.max(0, Math.min(newIndex, tracks.length));
1123
+ tracks.splice(clampedIndex, 0, track);
1124
+ const newMaster = tracks[0];
1125
+ const oldMasterId = state.tracks[0]?.id;
1126
+ if (newMaster.id !== oldMasterId) {
1127
+ const masterBarLength = newMaster.nativeBarLength;
1128
+ const masterTempo = newMaster.nativeBpm;
1129
+ set({
1130
+ masterTempo,
1131
+ originalTempo: masterTempo,
1132
+ masterBarLength48000: masterBarLength,
1133
+ tracks: recalculateSpeedRatios(tracks, masterBarLength)
1134
+ });
1135
+ } else {
1136
+ set({ tracks });
1137
+ }
1138
+ schedulePersist(get());
1139
+ },
1140
+ // ──────────────────────────────────────────────────────────────────
1141
+ // Deferred-load queue
1142
+ //
1143
+ // Pending mutations bypass the undo stack on purpose: promoting a
1144
+ // pending track is a network/decode operation that the user can
1145
+ // always reverse with a normal "remove track" (which IS undoable).
1146
+ // Conversely, blanking the queue on undo would let the user
1147
+ // accidentally "lose" tracks they have visible in the deferred
1148
+ // list — bad UX.
1149
+ //
1150
+ // Every mutator still re-persists so the queue survives a refresh
1151
+ // and stays in lock-step with the live tracks list.
1152
+ // ──────────────────────────────────────────────────────────────────
1153
+ setPendingTracks: (tracks) => {
1154
+ set({ pendingTracks: tracks.map((pt) => ({ ...pt })) });
1155
+ schedulePersist(get());
1156
+ },
1157
+ addPendingTracks: (tracks) => {
1158
+ if (tracks.length === 0) return;
1159
+ set((state) => ({
1160
+ pendingTracks: [...state.pendingTracks, ...tracks.map((pt) => ({ ...pt }))]
1161
+ }));
1162
+ schedulePersist(get());
1163
+ },
1164
+ removePendingTrackAt: (index) => {
1165
+ set((state) => {
1166
+ if (index < 0 || index >= state.pendingTracks.length) return state;
1167
+ const next = state.pendingTracks.slice();
1168
+ next.splice(index, 1);
1169
+ return { pendingTracks: next };
1170
+ });
1171
+ schedulePersist(get());
1172
+ },
1173
+ clearPendingTracks: () => {
1174
+ if (get().pendingTracks.length === 0) return;
1175
+ set({ pendingTracks: [] });
1176
+ schedulePersist(get());
1177
+ },
1178
+ // ──────────────────────────────────────────────────────────────────
1179
+ // Per-stem timeline editing
1180
+ // ──────────────────────────────────────────────────────────────────
1181
+ setStemTimelineStart: (trackId, stemIndex, bar) => {
1182
+ schedulePersist(get());
1183
+ set((state) => ({
1184
+ tracks: state.tracks.map(
1185
+ (t) => t.id === trackId ? {
1186
+ ...t,
1187
+ channels: t.channels.map(
1188
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, timelineStartBar: Math.round(bar) } : ch
1189
+ )
1190
+ } : t
1191
+ )
1192
+ }));
1193
+ },
1194
+ setStemTrim: (trackId, stemIndex, trimStart, trimEnd) => {
1195
+ schedulePersist(get());
1196
+ set((state) => ({
1197
+ tracks: state.tracks.map((t) => {
1198
+ if (t.id !== trackId) return t;
1199
+ const maxBar = t.barMapping.length - 1;
1200
+ return {
1201
+ ...t,
1202
+ channels: t.channels.map(
1203
+ (ch) => ch.stemIndex === stemIndex ? {
1204
+ ...ch,
1205
+ trimStartBar: Math.max(0, Math.min(Math.round(trimStart), maxBar)),
1206
+ trimEndBar: Math.max(0, Math.min(Math.round(trimEnd), maxBar))
1207
+ } : ch
1208
+ )
1209
+ };
1210
+ })
1211
+ }));
1212
+ },
1213
+ setTrackTimelineStart: (trackId, bar) => {
1214
+ set((state) => ({
1215
+ tracks: state.tracks.map(
1216
+ (t) => t.id === trackId ? {
1217
+ ...t,
1218
+ channels: t.channels.map((ch) => ({
1219
+ ...ch,
1220
+ timelineStartBar: Math.round(bar)
1221
+ }))
1222
+ } : t
1223
+ )
1224
+ }));
1225
+ },
1226
+ setTrackTrim: (trackId, trimStart, trimEnd) => {
1227
+ set((state) => ({
1228
+ tracks: state.tracks.map((t) => {
1229
+ if (t.id !== trackId) return t;
1230
+ const maxBar = t.barMapping.length - 1;
1231
+ return {
1232
+ ...t,
1233
+ channels: t.channels.map((ch) => ({
1234
+ ...ch,
1235
+ trimStartBar: Math.max(0, Math.min(Math.round(trimStart), maxBar)),
1236
+ trimEndBar: Math.max(0, Math.min(Math.round(trimEnd), maxBar))
1237
+ }))
1238
+ };
1239
+ })
1240
+ }));
1241
+ },
1242
+ // ──────────────────────────────────────────────────────────────────
1243
+ // Transport
1244
+ // ──────────────────────────────────────────────────────────────────
1245
+ setMixName: (name) => set({ mixName: name.trim() || "Untitled Mix" }),
1246
+ setToolMode: (mode) => set({ toolMode: mode }),
1247
+ setCompositeView: (on) => set({ compositeView: on }),
1248
+ toggleCompositeView: () => set((state) => ({ compositeView: !state.compositeView })),
1249
+ setSimilarTracksBalance: (v) => set({
1250
+ similarTracksBalance: Math.max(0, Math.min(1, v))
1251
+ }),
1252
+ setSimilarTracksPopularity: (v) => set({
1253
+ similarTracksPopularity: Math.max(0, Math.min(2, v))
1254
+ }),
1255
+ resetSimilarTracksTuning: () => set({
1256
+ similarTracksBalance: 0.5,
1257
+ similarTracksPopularity: 1
1258
+ }),
1259
+ getSimilarTracksQueryArgs: () => {
1260
+ const s = get();
1261
+ const ranking = {};
1262
+ if (s.similarTracksBalance !== 0.5) {
1263
+ ranking.genreWeight = Math.max(0, Math.min(1, 1 - s.similarTracksBalance));
1264
+ }
1265
+ if (s.similarTracksPopularity !== 1) {
1266
+ ranking.collaborativeStrength = Math.max(0, Math.min(2, s.similarTracksPopularity));
1267
+ }
1268
+ if (Object.keys(ranking).length === 0) return {};
1269
+ return { ranking };
1270
+ },
1271
+ setSelectedStem: (stem) => set({ selectedStem: stem }),
1272
+ setSelections: (ranges) => {
1273
+ if (ranges.length === 0) {
1274
+ set({ selections: [], selectionRange: null });
1275
+ return;
1276
+ }
1277
+ const trackId = ranges[0].trackId;
1278
+ const filtered = ranges.filter((r) => r.trackId === trackId && r.endBar > r.startBar);
1279
+ const byStem = /* @__PURE__ */ new Map();
1280
+ for (const r of filtered) byStem.set(r.stemIndex, r);
1281
+ const next = Array.from(byStem.values()).sort((a, b) => a.stemIndex - b.stemIndex);
1282
+ set({ selections: next, selectionRange: next[0] ?? null });
1283
+ },
1284
+ addSelection: (range) => {
1285
+ if (range.endBar <= range.startBar) return;
1286
+ const current = get().selections;
1287
+ if (current.length > 0 && current[0].trackId !== range.trackId) {
1288
+ set({ selections: [range], selectionRange: range });
1289
+ return;
1290
+ }
1291
+ const byStem = /* @__PURE__ */ new Map();
1292
+ for (const r of current) byStem.set(r.stemIndex, r);
1293
+ byStem.set(range.stemIndex, range);
1294
+ const next = Array.from(byStem.values()).sort((a, b) => a.stemIndex - b.stemIndex);
1295
+ set({ selections: next, selectionRange: next[0] ?? null });
1296
+ },
1297
+ clearSelections: () => set({ selections: [], selectionRange: null }),
1298
+ setSelectionRange: (range) => {
1299
+ if (!range || range.endBar <= range.startBar) {
1300
+ set({ selections: [], selectionRange: null });
1301
+ return;
1302
+ }
1303
+ set({ selections: [range], selectionRange: range });
1304
+ },
1305
+ setClipboardEntries: (entries) => {
1306
+ if (entries.length === 0) {
1307
+ set({ clipboardEntries: [], clipboard: null });
1308
+ return;
1309
+ }
1310
+ const trackId = entries[0].trackId;
1311
+ const filtered = entries.filter((e) => e.trackId === trackId && e.sourceEndBar > e.sourceStartBar);
1312
+ const byStem = /* @__PURE__ */ new Map();
1313
+ for (const e of filtered) byStem.set(e.stemIndex, e);
1314
+ const next = Array.from(byStem.values()).sort((a, b) => a.stemIndex - b.stemIndex);
1315
+ set({ clipboardEntries: next, clipboard: next[0] ?? null });
1316
+ },
1317
+ setClipboard: (entry) => {
1318
+ if (!entry) {
1319
+ set({ clipboardEntries: [], clipboard: null });
1320
+ return;
1321
+ }
1322
+ set({ clipboardEntries: [entry], clipboard: entry });
1323
+ },
1324
+ // ── Undo / Redo ──────────────────────────────────────────────────
1325
+ pushUndo: () => {
1326
+ const state = get();
1327
+ const snap = captureSnapshot(state);
1328
+ const stack = [...state._undoStack, snap];
1329
+ if (stack.length > MAX_UNDO) stack.shift();
1330
+ set({ _undoStack: stack, _redoStack: [] });
1331
+ },
1332
+ undo: () => {
1333
+ const state = get();
1334
+ if (state._undoStack.length === 0) return;
1335
+ const stack = [...state._undoStack];
1336
+ const snap = stack.pop();
1337
+ const redoSnap = captureSnapshot(state);
1338
+ set({
1339
+ ...snap,
1340
+ _undoStack: stack,
1341
+ _redoStack: [...state._redoStack, redoSnap]
1342
+ });
1343
+ schedulePersist(get());
1344
+ },
1345
+ redo: () => {
1346
+ const state = get();
1347
+ if (state._redoStack.length === 0) return;
1348
+ const stack = [...state._redoStack];
1349
+ const snap = stack.pop();
1350
+ const undoSnap = captureSnapshot(state);
1351
+ set({
1352
+ ...snap,
1353
+ _undoStack: [...state._undoStack, undoSnap],
1354
+ _redoStack: stack
1355
+ });
1356
+ schedulePersist(get());
1357
+ },
1358
+ canUndo: () => get()._undoStack.length > 0,
1359
+ canRedo: () => get()._redoStack.length > 0,
1360
+ setPlaying: (playing) => set({ isPlaying: playing }),
1361
+ setPlayheadBar: (bar) => set({ playheadBar: Math.max(0, bar) }),
1362
+ setMasterTempo: (bpm) => {
1363
+ const state = get();
1364
+ const masterBarLength = bpmToBarDuration(bpm);
1365
+ set({
1366
+ masterTempo: bpm,
1367
+ masterBarLength48000: masterBarLength,
1368
+ tracks: recalculateSpeedRatios(state.tracks, masterBarLength)
1369
+ });
1370
+ },
1371
+ setGlobalPlaybackRate: (rate) => set({ globalPlaybackRate: rate }),
1372
+ setLoopRegion: (region) => set({ loopRegion: region }),
1373
+ setBarSubdivisions: (n) => set({ barSubdivisions: Math.max(1, Math.round(n)) }),
1374
+ // ──────────────────────────────────────────────────────────────────
1375
+ // Session notes (timeline markers)
1376
+ //
1377
+ // Notes live on the session itself rather than per-track because they
1378
+ // describe moments in the listener's experience of the mix, not in any
1379
+ // single source recording. They never feed the audio engine — they're
1380
+ // pure visual annotation.
1381
+ // ──────────────────────────────────────────────────────────────────
1382
+ addNote: (note) => {
1383
+ get().pushUndo();
1384
+ const id = v4();
1385
+ const full = { ...note, id, createdAt: Date.now() };
1386
+ set({ notes: [...get().notes, full].sort((a, b) => a.startBar - b.startBar) });
1387
+ schedulePersist(get());
1388
+ return id;
1389
+ },
1390
+ updateNote: (id, patch) => {
1391
+ const existing = get().notes.find((n) => n.id === id);
1392
+ if (!existing) return;
1393
+ get().pushUndo();
1394
+ set({
1395
+ notes: get().notes.map((n) => n.id === id ? { ...n, ...patch } : n).sort((a, b) => a.startBar - b.startBar)
1396
+ });
1397
+ schedulePersist(get());
1398
+ },
1399
+ removeNote: (id) => {
1400
+ if (!get().notes.some((n) => n.id === id)) return;
1401
+ get().pushUndo();
1402
+ set({ notes: get().notes.filter((n) => n.id !== id) });
1403
+ schedulePersist(get());
1404
+ },
1405
+ // ──────────────────────────────────────────────────────────────────
1406
+ // Track controls
1407
+ // ──────────────────────────────────────────────────────────────────
1408
+ setTrackVolume: (trackId, volume) => set((state) => ({
1409
+ tracks: state.tracks.map(
1410
+ (t) => t.id === trackId ? { ...t, volume: Math.max(0, Math.min(2, volume)) } : t
1411
+ )
1412
+ })),
1413
+ setTrackMuted: (trackId, muted) => set((state) => ({
1414
+ tracks: state.tracks.map((t) => t.id === trackId ? { ...t, muted } : t)
1415
+ })),
1416
+ setTrackSoloed: (trackId, soloed) => set((state) => ({
1417
+ tracks: state.tracks.map((t) => t.id === trackId ? { ...t, soloed } : t)
1418
+ })),
1419
+ setTrackExpanded: (trackId, expanded) => set((state) => ({
1420
+ tracks: state.tracks.map((t) => t.id === trackId ? { ...t, expanded } : t)
1421
+ })),
1422
+ setTrackTranspose: (trackId, semitones) => {
1423
+ set((state) => ({
1424
+ tracks: state.tracks.map(
1425
+ (t) => t.id === trackId ? { ...t, transposeSemitones: Math.max(-12, Math.min(12, semitones)) } : t
1426
+ )
1427
+ }));
1428
+ schedulePersist(get());
1429
+ },
1430
+ setTrackPitchSettings: (trackId, settings) => {
1431
+ set((state) => ({
1432
+ tracks: state.tracks.map(
1433
+ (t) => t.id === trackId ? { ...t, pitchSettings: { ...settings } } : t
1434
+ )
1435
+ }));
1436
+ schedulePersist(get());
1437
+ },
1438
+ setPerBarPitchCompensation: (trackId, enabled) => {
1439
+ set((state) => ({
1440
+ tracks: state.tracks.map(
1441
+ (t) => t.id === trackId ? { ...t, perBarPitchCompensation: enabled } : t
1442
+ )
1443
+ }));
1444
+ schedulePersist(get());
1445
+ },
1446
+ // ──────────────────────────────────────────────────────────────────
1447
+ // Beat grid alignment
1448
+ // ──────────────────────────────────────────────────────────────────
1449
+ setTrackBeatGridOffset: (trackId, offsetBeats) => {
1450
+ get().pushUndo();
1451
+ set((state2) => ({
1452
+ tracks: state2.tracks.map((t) => {
1453
+ if (t.id !== trackId) return t;
1454
+ const beats = t.trackData.beats;
1455
+ if (!beats || beats.length === 0) return t;
1456
+ const newBarMapping = composeBarsWithOffset(beats, offsetBeats);
1457
+ if (newBarMapping.length < 2) return t;
1458
+ const newNativeBarLength = medianBarDuration(newBarMapping) || t.nativeBarLength;
1459
+ const trimEnd = Math.max(0, newBarMapping.length - 1);
1460
+ return {
1461
+ ...t,
1462
+ beatGridOffsetBeats: offsetBeats,
1463
+ barMapping: newBarMapping,
1464
+ nativeBarLength: newNativeBarLength,
1465
+ channels: t.channels.map((ch) => ({
1466
+ ...ch,
1467
+ trimEndBar: Math.min(ch.trimEndBar, trimEnd),
1468
+ trimStartBar: Math.min(ch.trimStartBar, trimEnd)
1469
+ }))
1470
+ };
1471
+ })
1472
+ }));
1473
+ const state = get();
1474
+ if (state.masterBarLength48000 > 0) {
1475
+ set({ tracks: recalculateSpeedRatios(state.tracks, state.masterBarLength48000) });
1476
+ }
1477
+ schedulePersist(get());
1478
+ },
1479
+ replaceTrackBeats: (trackId, newBeats) => {
1480
+ if (!newBeats || newBeats.length < 2) return;
1481
+ get().pushUndo();
1482
+ const newBarMapping = composeBarsFromBeats(newBeats);
1483
+ if (newBarMapping.length < 2) return;
1484
+ const newNativeBarLength = medianBarDuration(newBarMapping);
1485
+ if (!newNativeBarLength) return;
1486
+ const newNativeBpm = barDurationToBpm(newNativeBarLength);
1487
+ set((state) => {
1488
+ const isLeadTrack = state.tracks.length > 0 && state.tracks[0].id === trackId;
1489
+ const masterBarLength = isLeadTrack ? newNativeBarLength : state.masterBarLength48000;
1490
+ const masterTempo = isLeadTrack ? newNativeBpm || state.masterTempo : state.masterTempo;
1491
+ const updatedTracks = state.tracks.map((t) => {
1492
+ if (t.id !== trackId) return t;
1493
+ const trimEnd = Math.max(0, newBarMapping.length - 1);
1494
+ return {
1495
+ ...t,
1496
+ beatGridOffsetBeats: 0,
1497
+ barMapping: newBarMapping,
1498
+ nativeBarLength: newNativeBarLength,
1499
+ nativeBpm: newNativeBpm || t.nativeBpm,
1500
+ trackData: { ...t.trackData, beats: newBeats },
1501
+ channels: t.channels.map((ch) => ({
1502
+ ...ch,
1503
+ trimEndBar: Math.min(ch.trimEndBar, trimEnd),
1504
+ trimStartBar: Math.min(ch.trimStartBar, trimEnd)
1505
+ }))
1506
+ };
1507
+ });
1508
+ return {
1509
+ masterBarLength48000: masterBarLength,
1510
+ masterTempo,
1511
+ tracks: recalculateSpeedRatios(updatedTracks, masterBarLength)
1512
+ };
1513
+ });
1514
+ schedulePersist(get());
1515
+ },
1516
+ // ──────────────────────────────────────────────────────────────────
1517
+ // Channel (stem) controls
1518
+ // ──────────────────────────────────────────────────────────────────
1519
+ setChannelVolume: (trackId, stemIndex, volume) => set((state) => ({
1520
+ tracks: state.tracks.map(
1521
+ (t) => t.id === trackId ? {
1522
+ ...t,
1523
+ channels: t.channels.map(
1524
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, volume: Math.max(0, Math.min(2, volume)) } : ch
1525
+ )
1526
+ } : t
1527
+ )
1528
+ })),
1529
+ setChannelMuted: (trackId, stemIndex, muted) => set((state) => ({
1530
+ tracks: state.tracks.map(
1531
+ (t) => t.id === trackId ? {
1532
+ ...t,
1533
+ channels: t.channels.map(
1534
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, muted } : ch
1535
+ )
1536
+ } : t
1537
+ )
1538
+ })),
1539
+ setChannelSoloed: (trackId, stemIndex, soloed) => set((state) => ({
1540
+ tracks: state.tracks.map(
1541
+ (t) => t.id === trackId ? {
1542
+ ...t,
1543
+ channels: t.channels.map(
1544
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, soloed } : ch
1545
+ )
1546
+ } : t
1547
+ )
1548
+ })),
1549
+ setChannelVisible: (trackId, stemIndex, visible) => set((state) => ({
1550
+ tracks: state.tracks.map(
1551
+ (t) => t.id === trackId ? {
1552
+ ...t,
1553
+ channels: t.channels.map(
1554
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, visible } : ch
1555
+ )
1556
+ } : t
1557
+ )
1558
+ })),
1559
+ setChannelEffects: (trackId, stemIndex, effects) => set((state) => ({
1560
+ tracks: state.tracks.map(
1561
+ (t) => t.id === trackId ? {
1562
+ ...t,
1563
+ channels: t.channels.map(
1564
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, effects } : ch
1565
+ )
1566
+ } : t
1567
+ )
1568
+ })),
1569
+ // ──────────────────────────────────────────────────────────────────
1570
+ // Mute regions
1571
+ // ──────────────────────────────────────────────────────────────────
1572
+ addMuteRegion: (trackId, stemIndex, region) => {
1573
+ get().pushUndo();
1574
+ set((state) => ({
1575
+ tracks: state.tracks.map(
1576
+ (t) => t.id === trackId ? {
1577
+ ...t,
1578
+ channels: t.channels.map((ch) => {
1579
+ if (ch.stemIndex !== stemIndex) return ch;
1580
+ const merged = mergeMuteRegions([...ch.muteRegions, region]);
1581
+ return { ...ch, muteRegions: merged };
1582
+ })
1583
+ } : t
1584
+ )
1585
+ }));
1586
+ schedulePersist(get());
1587
+ },
1588
+ removeMuteRegion: (trackId, stemIndex, regionIndex) => {
1589
+ get().pushUndo();
1590
+ set((state) => ({
1591
+ tracks: state.tracks.map(
1592
+ (t) => t.id === trackId ? {
1593
+ ...t,
1594
+ channels: t.channels.map(
1595
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, muteRegions: ch.muteRegions.filter((_, i) => i !== regionIndex) } : ch
1596
+ )
1597
+ } : t
1598
+ )
1599
+ }));
1600
+ schedulePersist(get());
1601
+ },
1602
+ clearMuteRegions: (trackId, stemIndex) => {
1603
+ get().pushUndo();
1604
+ set((state) => ({
1605
+ tracks: state.tracks.map(
1606
+ (t) => t.id === trackId ? {
1607
+ ...t,
1608
+ channels: t.channels.map(
1609
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, muteRegions: [] } : ch
1610
+ )
1611
+ } : t
1612
+ )
1613
+ }));
1614
+ schedulePersist(get());
1615
+ },
1616
+ setMuteRegionsBatch: (trackId, stemIndex, regions) => {
1617
+ set((state) => ({
1618
+ tracks: state.tracks.map(
1619
+ (t) => t.id === trackId ? {
1620
+ ...t,
1621
+ channels: t.channels.map(
1622
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, muteRegions: mergeMuteRegions(regions) } : ch
1623
+ )
1624
+ } : t
1625
+ )
1626
+ }));
1627
+ },
1628
+ splitStemAtBar: (trackId, stemIndex, bar) => {
1629
+ get().pushUndo();
1630
+ const state = get();
1631
+ const track = state.tracks.find((t) => t.id === trackId);
1632
+ if (!track) return;
1633
+ const ch = track.channels.find((c) => c.stemIndex === stemIndex);
1634
+ if (!ch) return;
1635
+ const localBar = Math.round(bar);
1636
+ if (localBar <= ch.trimStartBar || localBar >= ch.trimEndBar) return;
1637
+ const region = { startBar: localBar, endBar: localBar + 1 };
1638
+ const merged = mergeMuteRegions([...ch.muteRegions, region]);
1639
+ set({
1640
+ tracks: state.tracks.map(
1641
+ (t) => t.id === trackId ? {
1642
+ ...t,
1643
+ channels: t.channels.map(
1644
+ (c) => c.stemIndex === stemIndex ? { ...c, muteRegions: merged } : c
1645
+ )
1646
+ } : t
1647
+ )
1648
+ });
1649
+ schedulePersist(get());
1650
+ },
1651
+ // ──────────────────────────────────────────────────────────────────
1652
+ // Split points (logical clip boundaries — pure visual, no audio side
1653
+ // effect). The audio engine ignores these entirely; they only feed
1654
+ // into computeClips() so the UI knows where to break a stem into
1655
+ // independently-movable clips.
1656
+ // ──────────────────────────────────────────────────────────────────
1657
+ addSplitPoint: (trackId, stemIndex, localBar) => {
1658
+ const state = get();
1659
+ const track = state.tracks.find((t) => t.id === trackId);
1660
+ if (!track) return;
1661
+ const ch = track.channels.find((c) => c.stemIndex === stemIndex);
1662
+ if (!ch) return;
1663
+ if (localBar <= ch.trimStartBar || localBar >= ch.trimEndBar) return;
1664
+ const existing = ch.splitPoints ?? [];
1665
+ if (existing.some((p) => Math.abs(p - localBar) < 0.015625)) return;
1666
+ get().pushUndo();
1667
+ const merged = [...existing, localBar].sort((a, b) => a - b);
1668
+ set({
1669
+ tracks: get().tracks.map(
1670
+ (t) => t.id === trackId ? {
1671
+ ...t,
1672
+ channels: t.channels.map(
1673
+ (c) => c.stemIndex === stemIndex ? { ...c, splitPoints: merged } : c
1674
+ )
1675
+ } : t
1676
+ )
1677
+ });
1678
+ schedulePersist(get());
1679
+ },
1680
+ setSplitPointsBatch: (trackId, stemIndex, points, pushUndo = true) => {
1681
+ const state = get();
1682
+ const track = state.tracks.find((t) => t.id === trackId);
1683
+ if (!track) return;
1684
+ const ch = track.channels.find((c) => c.stemIndex === stemIndex);
1685
+ if (!ch) return;
1686
+ if (pushUndo) get().pushUndo();
1687
+ const cleaned = Array.from(new Set(points.filter(
1688
+ (p) => p > ch.trimStartBar && p < ch.trimEndBar
1689
+ ))).sort((a, b) => a - b);
1690
+ set({
1691
+ tracks: get().tracks.map(
1692
+ (t) => t.id === trackId ? {
1693
+ ...t,
1694
+ channels: t.channels.map(
1695
+ (c) => c.stemIndex === stemIndex ? { ...c, splitPoints: cleaned } : c
1696
+ )
1697
+ } : t
1698
+ )
1699
+ });
1700
+ schedulePersist(get());
1701
+ },
1702
+ clearSplitPoints: (trackId, stemIndex) => {
1703
+ const state = get();
1704
+ const track = state.tracks.find((t) => t.id === trackId);
1705
+ if (!track) return;
1706
+ const ch = track.channels.find((c) => c.stemIndex === stemIndex);
1707
+ if (!ch || !ch.splitPoints || ch.splitPoints.length === 0) return;
1708
+ get().pushUndo();
1709
+ set({
1710
+ tracks: get().tracks.map(
1711
+ (t) => t.id === trackId ? {
1712
+ ...t,
1713
+ channels: t.channels.map(
1714
+ (c) => c.stemIndex === stemIndex ? { ...c, splitPoints: [] } : c
1715
+ )
1716
+ } : t
1717
+ )
1718
+ });
1719
+ schedulePersist(get());
1720
+ },
1721
+ // ──────────────────────────────────────────────────────────────────
1722
+ // Resampled regions
1723
+ // ──────────────────────────────────────────────────────────────────
1724
+ addResampledRegion: (trackId, stemIndex, region) => {
1725
+ set((state) => ({
1726
+ tracks: state.tracks.map(
1727
+ (t) => t.id === trackId ? {
1728
+ ...t,
1729
+ channels: t.channels.map(
1730
+ (ch) => ch.stemIndex === stemIndex ? { ...ch, resampledRegions: [...ch.resampledRegions ?? [], region] } : ch
1731
+ )
1732
+ } : t
1733
+ )
1734
+ }));
1735
+ schedulePersist(get());
1736
+ },
1737
+ // ──────────────────────────────────────────────────────────────────
1738
+ // Derived
1739
+ // ──────────────────────────────────────────────────────────────────
1740
+ getTimelineLength: () => {
1741
+ const state = get();
1742
+ if (state.tracks.length === 0) return 0;
1743
+ let maxEnd = 0;
1744
+ for (const t of state.tracks) {
1745
+ for (const ch of t.channels) {
1746
+ if (!ch.visible) continue;
1747
+ const end = ch.timelineStartBar + (ch.trimEndBar - ch.trimStartBar + 1);
1748
+ if (end > maxEnd) maxEnd = end;
1749
+ }
1750
+ }
1751
+ return maxEnd;
1752
+ },
1753
+ getTrackById: (trackId) => {
1754
+ return get().tracks.find((t) => t.id === trackId);
1755
+ },
1756
+ getChannel: (trackId, stemIndex) => {
1757
+ const track = get().tracks.find((t) => t.id === trackId);
1758
+ return track?.channels.find((ch) => ch.stemIndex === stemIndex);
1759
+ }
1760
+ }));
1761
+
1762
+ export { DEFAULT_PITCH_SETTINGS, NOTE_COLORS, NOTE_EMOJI_PALETTE, PitchShiftStrategy, SEGMENT_COLORS, SEGMENT_LABELS, SEGMENT_TYPE, STEM_BUFFER_KEYS, STEM_COLORS, STEM_LABELS, STEM_TYPES, barDurationToBpm, barToSample, bpmToBarDuration, buildPersistedSession, cacheAudioBuffer, calculateTempoMatch, camelotMatchType, clearAll, composeBarsFromBeats, composeBarsFromBeatsSmoothed, computeBarSpeedRatio, computeClips, computePitchShift, computeTransposeSuggestions, createDefaultChannels, deleteProjectFromLibrary, exportProject, findFirstSectionOfType, getCachedAudioBuffer, getSelectionBoundsBars, importProject, keyToSemitones, listProjects, loadProjectFromLibrary, loadSession, medianBarDuration, parseStemIdKey, playbackRateToSemitones, resolveSampleLocOfDownbeatFromBarIndex, resolveTimeOfDownbeatFromBarIndex, saveProjectToLibrary, segmentInfoToSections, semitonesToKey, semitonesToPitchFactor, stemIdKey, toCamelot, transposeKey, useDAWSessionStore };
1763
+ //# sourceMappingURL=chunk-KCOOE2OP.js.map
1764
+ //# sourceMappingURL=chunk-KCOOE2OP.js.map