@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.
- package/README.md +253 -0
- package/dist/chat-actions-54Z6URC4.js +7 -0
- package/dist/chat-actions-54Z6URC4.js.map +1 -0
- package/dist/chunk-56PWIP7O.js +1029 -0
- package/dist/chunk-56PWIP7O.js.map +1 -0
- package/dist/chunk-AAVC7KUW.js +145 -0
- package/dist/chunk-AAVC7KUW.js.map +1 -0
- package/dist/chunk-KCOOE2OP.js +1764 -0
- package/dist/chunk-KCOOE2OP.js.map +1 -0
- package/dist/chunk-LO74ZJ4H.js +23923 -0
- package/dist/chunk-LO74ZJ4H.js.map +1 -0
- package/dist/chunk-OFGZURP6.js +247 -0
- package/dist/chunk-OFGZURP6.js.map +1 -0
- package/dist/chunk-OYNES5W3.js +3085 -0
- package/dist/chunk-OYNES5W3.js.map +1 -0
- package/dist/chunk-QQ5NZTHT.js +336 -0
- package/dist/chunk-QQ5NZTHT.js.map +1 -0
- package/dist/chunk-TBXCZFAY.js +13713 -0
- package/dist/chunk-TBXCZFAY.js.map +1 -0
- package/dist/chunk-U44X6QP5.js +281 -0
- package/dist/chunk-U44X6QP5.js.map +1 -0
- package/dist/chunk-UKMELGZL.js +27 -0
- package/dist/chunk-UKMELGZL.js.map +1 -0
- package/dist/components/DAWView.d.ts +19 -0
- package/dist/components/DAWView.js +11 -0
- package/dist/components/DAWView.js.map +1 -0
- package/dist/daw-controller-BjRWcTol.d.ts +339 -0
- package/dist/engine/daw-controller.d.ts +3 -0
- package/dist/engine/daw-controller.js +5 -0
- package/dist/engine/daw-controller.js.map +1 -0
- package/dist/engine/daw-import-stem-fm-config.d.ts +224 -0
- package/dist/engine/daw-import-stem-fm-config.js +7 -0
- package/dist/engine/daw-import-stem-fm-config.js.map +1 -0
- package/dist/fetchStationTracks-SKFT4V3U.js +3 -0
- package/dist/fetchStationTracks-SKFT4V3U.js.map +1 -0
- package/dist/index.d.ts +308 -0
- package/dist/index.js +332 -0
- package/dist/index.js.map +1 -0
- package/dist/interface-DaRj7RkY.d.ts +66 -0
- package/dist/interfaces-5ZlG0Y4Y.d.ts +549 -0
- package/dist/media-session-XTP6PP7Q.js +3 -0
- package/dist/media-session-XTP6PP7Q.js.map +1 -0
- package/dist/note-detection-PPLM7R2H.js +148 -0
- package/dist/note-detection-PPLM7R2H.js.map +1 -0
- package/dist/sampler-audio-B7MBG3YN.js +3 -0
- package/dist/sampler-audio-B7MBG3YN.js.map +1 -0
- package/dist/sampler-store-QPHANXYP.js +3 -0
- package/dist/sampler-store-QPHANXYP.js.map +1 -0
- package/dist/services/track-search-api.d.ts +152 -0
- package/dist/services/track-search-api.js +4 -0
- package/dist/services/track-search-api.js.map +1 -0
- package/dist/store/daw-auth-store.d.ts +31 -0
- package/dist/store/daw-auth-store.js +3 -0
- package/dist/store/daw-auth-store.js.map +1 -0
- package/dist/store/daw-session-store.d.ts +255 -0
- package/dist/store/daw-session-store.js +3 -0
- package/dist/store/daw-session-store.js.map +1 -0
- package/dist/vite/index.d.ts +46 -0
- package/dist/vite/index.js +94 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/workers/analysis-worker.js +379 -0
- package/dist/workers/buffer-player-processor-202602.lavv8e32-ts.js +1 -0
- package/dist/workers/daw-stem-processor.js +228 -0
- package/dist/workers/manifest.json +10 -0
- package/dist/workers/phase-vocoder3.js +920 -0
- package/dist/workers/realtime-pitch-shift-processor.js +2 -0
- package/package.json +151 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { DAWController } from './chunk-OYNES5W3.js';
|
|
2
|
+
import { fetchTrackForDAW } from './chunk-56PWIP7O.js';
|
|
3
|
+
import { useDAWSessionStore } from './chunk-KCOOE2OP.js';
|
|
4
|
+
|
|
5
|
+
// src/daw/engine/daw-import-stem-fm-config.ts
|
|
6
|
+
var STEM_NAMES = ["other", "vocals", "bass", "drums"];
|
|
7
|
+
function isModifierEvent(e) {
|
|
8
|
+
return e.category === "modifier";
|
|
9
|
+
}
|
|
10
|
+
function modifierEventsForTarget(session, type, trackId, stem) {
|
|
11
|
+
return session.events.filter(isModifierEvent).filter((e) => e.type === type && e.target.trackId === trackId).filter((e) => stem === void 0 ? !e.target.stem : e.target.stem === stem).sort((a, b) => a.schedule.triggerTime.bar - b.schedule.triggerTime.bar);
|
|
12
|
+
}
|
|
13
|
+
function resolveSessionAnchor(session, trackChannelIndex, trackId) {
|
|
14
|
+
const anchors = session.originAnchors;
|
|
15
|
+
if (anchors && trackChannelIndex < anchors.length) {
|
|
16
|
+
return anchors[trackChannelIndex].originalBarIndex;
|
|
17
|
+
}
|
|
18
|
+
const rateEvents = modifierEventsForTarget(session, "rate", trackId);
|
|
19
|
+
const firstWithOrigin = rateEvents.find((e) => e.schedule.triggerOrigin);
|
|
20
|
+
return firstWithOrigin?.schedule.triggerOrigin?.originalBarIndex ?? 0;
|
|
21
|
+
}
|
|
22
|
+
function reduceVolumeEvents(session, trackId, stem) {
|
|
23
|
+
const events = modifierEventsForTarget(session, "volume", trackId, stem);
|
|
24
|
+
const sessionLen = session.length.bars;
|
|
25
|
+
if (events.length === 0) {
|
|
26
|
+
return { channelVolume: 1, zeroRegionsInSession: [] };
|
|
27
|
+
}
|
|
28
|
+
const pieces = [];
|
|
29
|
+
let cursor = 0;
|
|
30
|
+
let curValue = events[0].behavior.startValue;
|
|
31
|
+
for (const ev of events) {
|
|
32
|
+
const evBar = ev.schedule.triggerTime.bar - 1;
|
|
33
|
+
if (evBar > cursor) {
|
|
34
|
+
pieces.push({ startBar: cursor, endBar: evBar, value: curValue });
|
|
35
|
+
cursor = evBar;
|
|
36
|
+
}
|
|
37
|
+
const rampEndBar = evBar + (ev.schedule.length.bars || 0);
|
|
38
|
+
if (rampEndBar > cursor) {
|
|
39
|
+
const rampValue = Math.max(ev.behavior.startValue, ev.behavior.endValue);
|
|
40
|
+
pieces.push({ startBar: cursor, endBar: rampEndBar, value: rampValue });
|
|
41
|
+
cursor = rampEndBar;
|
|
42
|
+
}
|
|
43
|
+
curValue = ev.behavior.endValue;
|
|
44
|
+
}
|
|
45
|
+
if (cursor < sessionLen) {
|
|
46
|
+
pieces.push({ startBar: cursor, endBar: sessionLen, value: curValue });
|
|
47
|
+
}
|
|
48
|
+
let channelVolume = 0;
|
|
49
|
+
for (const p of pieces) {
|
|
50
|
+
if (p.value > channelVolume) channelVolume = p.value;
|
|
51
|
+
}
|
|
52
|
+
if (channelVolume === 0) channelVolume = 1;
|
|
53
|
+
const zeroRegionsInSession = [];
|
|
54
|
+
let zeroStart = null;
|
|
55
|
+
for (const p of pieces) {
|
|
56
|
+
if (p.value <= 1e-3) {
|
|
57
|
+
if (zeroStart === null) zeroStart = p.startBar;
|
|
58
|
+
} else if (zeroStart !== null) {
|
|
59
|
+
zeroRegionsInSession.push({ startBar: zeroStart, endBar: p.startBar });
|
|
60
|
+
zeroStart = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (zeroStart !== null) {
|
|
64
|
+
zeroRegionsInSession.push({ startBar: zeroStart, endBar: sessionLen });
|
|
65
|
+
}
|
|
66
|
+
return { channelVolume, zeroRegionsInSession };
|
|
67
|
+
}
|
|
68
|
+
function averageRateForTrack(session, trackId) {
|
|
69
|
+
const events = modifierEventsForTarget(session, "rate", trackId);
|
|
70
|
+
if (events.length === 0) return 1;
|
|
71
|
+
let sum = 0;
|
|
72
|
+
for (const e of events) sum += e.behavior.startValue || 1;
|
|
73
|
+
return sum / events.length;
|
|
74
|
+
}
|
|
75
|
+
function zeroRegionsToMuteRegions(zeroRegions, sourceStartBar, sessionLen) {
|
|
76
|
+
if (sessionLen <= 0) return [];
|
|
77
|
+
return zeroRegions.map((z) => ({
|
|
78
|
+
startBar: sourceStartBar + Math.max(0, z.startBar),
|
|
79
|
+
endBar: sourceStartBar + Math.min(sessionLen, z.endBar)
|
|
80
|
+
})).filter((r) => r.endBar > r.startBar);
|
|
81
|
+
}
|
|
82
|
+
function planClipsFromStemFmConfig(config) {
|
|
83
|
+
const clips = [];
|
|
84
|
+
const seenTrackIds = /* @__PURE__ */ new Set();
|
|
85
|
+
let timelineCursor = 0;
|
|
86
|
+
for (let sessionIdx = 0; sessionIdx < config.length; sessionIdx++) {
|
|
87
|
+
const session = config[sessionIdx];
|
|
88
|
+
const sessionStartBar = timelineCursor;
|
|
89
|
+
const sessionLen = session.bars ?? session.length.bars;
|
|
90
|
+
for (let tcIdx = 0; tcIdx < session.trackChannels.length; tcIdx++) {
|
|
91
|
+
const trackId = session.trackChannels[tcIdx].id;
|
|
92
|
+
seenTrackIds.add(trackId);
|
|
93
|
+
const sourceStartBar = resolveSessionAnchor(session, tcIdx, trackId);
|
|
94
|
+
const sourceEndBar = sourceStartBar + Math.max(1, sessionLen);
|
|
95
|
+
const perStem = STEM_NAMES.map((s) => reduceVolumeEvents(session, trackId, s));
|
|
96
|
+
const channelVolumes = [
|
|
97
|
+
perStem[0].channelVolume,
|
|
98
|
+
perStem[1].channelVolume,
|
|
99
|
+
perStem[2].channelVolume,
|
|
100
|
+
perStem[3].channelVolume
|
|
101
|
+
];
|
|
102
|
+
const stemMuteRegions = [
|
|
103
|
+
zeroRegionsToMuteRegions(perStem[0].zeroRegionsInSession, sourceStartBar, sessionLen),
|
|
104
|
+
zeroRegionsToMuteRegions(perStem[1].zeroRegionsInSession, sourceStartBar, sessionLen),
|
|
105
|
+
zeroRegionsToMuteRegions(perStem[2].zeroRegionsInSession, sourceStartBar, sessionLen),
|
|
106
|
+
zeroRegionsToMuteRegions(perStem[3].zeroRegionsInSession, sourceStartBar, sessionLen)
|
|
107
|
+
];
|
|
108
|
+
const pitchEvents = modifierEventsForTarget(session, "pitch", trackId);
|
|
109
|
+
const transposeSemitones = pitchEvents.length > 0 ? pitchEvents[0].behavior.endValue : 0;
|
|
110
|
+
clips.push({
|
|
111
|
+
sessionIndex: sessionIdx,
|
|
112
|
+
sourceTrackId: trackId,
|
|
113
|
+
timelineStartBar: sessionStartBar - sourceStartBar,
|
|
114
|
+
sourceStartBar,
|
|
115
|
+
sourceEndBar,
|
|
116
|
+
stemVolumes: channelVolumes,
|
|
117
|
+
stemMuteRegions,
|
|
118
|
+
transposeSemitones,
|
|
119
|
+
avgRate: averageRateForTrack(session, trackId)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
timelineCursor += sessionLen;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
clips,
|
|
126
|
+
totalTimelineBars: timelineCursor,
|
|
127
|
+
trackIds: Array.from(seenTrackIds)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function isStemFmMixConfig(value) {
|
|
131
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
132
|
+
const first = value[0];
|
|
133
|
+
if (!first) return false;
|
|
134
|
+
if (first.type !== "playback" && first.type !== "transition") return false;
|
|
135
|
+
if (!Array.isArray(first.trackChannels)) return false;
|
|
136
|
+
if (!first.length || typeof first.length.bars !== "number") return false;
|
|
137
|
+
if (!Array.isArray(first.events)) return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
async function importStemFmMixConfig(config, callbacks) {
|
|
141
|
+
const controller = DAWController.getInstance();
|
|
142
|
+
if (!controller.isReady()) await controller.init();
|
|
143
|
+
callbacks?.onStatus?.("Planning clips from stem.fm config\u2026");
|
|
144
|
+
callbacks?.onPercent?.(0);
|
|
145
|
+
const maxSessions = callbacks?.maxSessions ?? Infinity;
|
|
146
|
+
const effectiveConfig = maxSessions < config.length ? config.slice(0, maxSessions) : config;
|
|
147
|
+
if (effectiveConfig.length < config.length) {
|
|
148
|
+
console.info(
|
|
149
|
+
`[importStemFmMixConfig] Truncating ${config.length} sessions \u2192 first ${effectiveConfig.length} (maxSessions=${maxSessions}).`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const plan = planClipsFromStemFmConfig(effectiveConfig);
|
|
153
|
+
const { clips, totalTimelineBars, trackIds } = plan;
|
|
154
|
+
if (clips.length === 0) {
|
|
155
|
+
throw new Error("stem.fm config produced no audible clips");
|
|
156
|
+
}
|
|
157
|
+
const trackDataCache = /* @__PURE__ */ new Map();
|
|
158
|
+
callbacks?.onStatus?.(`Resolving ${trackIds.length} track${trackIds.length === 1 ? "" : "s"}\u2026`);
|
|
159
|
+
for (let i = 0; i < trackIds.length; i++) {
|
|
160
|
+
const id = trackIds[i];
|
|
161
|
+
callbacks?.onPercent?.(Math.round(i / trackIds.length * 15));
|
|
162
|
+
try {
|
|
163
|
+
const td = await fetchTrackForDAW(id);
|
|
164
|
+
trackDataCache.set(id, td);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.warn("[importStemFmMixConfig] failed to fetch track", id, err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const decodeProgress = new Array(clips.length).fill(0);
|
|
170
|
+
const emitAggregateProgress = () => {
|
|
171
|
+
const avg = decodeProgress.reduce((a, b) => a + b, 0) / decodeProgress.length;
|
|
172
|
+
callbacks?.onPercent?.(15 + Math.round(avg / 100 * 80));
|
|
173
|
+
};
|
|
174
|
+
let addedTrackInstances = 0;
|
|
175
|
+
const decodeCompletePromises = [];
|
|
176
|
+
for (let i = 0; i < clips.length; i++) {
|
|
177
|
+
const clip = clips[i];
|
|
178
|
+
const trackData = trackDataCache.get(clip.sourceTrackId);
|
|
179
|
+
if (!trackData) {
|
|
180
|
+
decodeProgress[i] = 100;
|
|
181
|
+
emitAggregateProgress();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
callbacks?.onStatus?.(
|
|
185
|
+
`Loading clip ${i + 1}/${clips.length} from "${trackData.name || clip.sourceTrackId}"\u2026`
|
|
186
|
+
);
|
|
187
|
+
const idx = i;
|
|
188
|
+
const onProgress = (p) => {
|
|
189
|
+
decodeProgress[idx] = p.percent;
|
|
190
|
+
emitAggregateProgress();
|
|
191
|
+
if (p.detail) callbacks?.onStatus?.(`${trackData.name}: ${p.detail}`);
|
|
192
|
+
};
|
|
193
|
+
const dawTrack = await controller.addTrack(trackData, onProgress);
|
|
194
|
+
if (!dawTrack) {
|
|
195
|
+
decodeProgress[idx] = 100;
|
|
196
|
+
emitAggregateProgress();
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
addedTrackInstances++;
|
|
200
|
+
applyClipStateToTrack(controller, dawTrack.id, dawTrack.barMapping.length, clip);
|
|
201
|
+
const trackId = dawTrack.id;
|
|
202
|
+
decodeCompletePromises.push(
|
|
203
|
+
Promise.race([
|
|
204
|
+
controller.whenTrackLoaded(trackId).then(() => {
|
|
205
|
+
decodeProgress[idx] = 100;
|
|
206
|
+
emitAggregateProgress();
|
|
207
|
+
}),
|
|
208
|
+
new Promise((resolve) => setTimeout(() => {
|
|
209
|
+
console.warn(
|
|
210
|
+
`[importStemFmMixConfig] decode timeout for track ${trackId} (${trackData.name}); proceeding with partial audio.`
|
|
211
|
+
);
|
|
212
|
+
decodeProgress[idx] = 100;
|
|
213
|
+
emitAggregateProgress();
|
|
214
|
+
resolve();
|
|
215
|
+
}, 5 * 60 * 1e3))
|
|
216
|
+
]).catch((err) => {
|
|
217
|
+
console.warn(`[importStemFmMixConfig] track ${trackId} aborted:`, err);
|
|
218
|
+
decodeProgress[idx] = 100;
|
|
219
|
+
emitAggregateProgress();
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
callbacks?.onStatus?.(`Decoding ${addedTrackInstances} clip${addedTrackInstances === 1 ? "" : "s"}\u2026`);
|
|
224
|
+
await Promise.all(decodeCompletePromises);
|
|
225
|
+
callbacks?.onPercent?.(95);
|
|
226
|
+
callbacks?.onStatus?.("Finalising session\u2026");
|
|
227
|
+
const store = useDAWSessionStore.getState();
|
|
228
|
+
store.setMixName("stem.fm imported mix");
|
|
229
|
+
controller.seekToBar(0);
|
|
230
|
+
useDAWSessionStore.setState({ _undoStack: [], _redoStack: [] });
|
|
231
|
+
callbacks?.onPercent?.(100);
|
|
232
|
+
callbacks?.onStatus?.(
|
|
233
|
+
`Imported ${addedTrackInstances} clip${addedTrackInstances === 1 ? "" : "s"} (${totalTimelineBars} bars).`
|
|
234
|
+
);
|
|
235
|
+
return { clips, totalTimelineBars, addedTrackInstances };
|
|
236
|
+
}
|
|
237
|
+
function applyClipStateToTrack(controller, trackId, totalSourceBars, clip) {
|
|
238
|
+
const trimStart = Math.max(0, Math.min(totalSourceBars - 1, clip.sourceStartBar));
|
|
239
|
+
const trimEnd = Math.max(trimStart + 1, Math.min(totalSourceBars, clip.sourceEndBar));
|
|
240
|
+
const tlStart = Math.round(clip.timelineStartBar);
|
|
241
|
+
const stemVolumes = clip.stemVolumes;
|
|
242
|
+
const mutePlans = clip.stemMuteRegions;
|
|
243
|
+
useDAWSessionStore.setState((state) => ({
|
|
244
|
+
tracks: state.tracks.map((t) => {
|
|
245
|
+
if (t.id !== trackId) return t;
|
|
246
|
+
return {
|
|
247
|
+
...t,
|
|
248
|
+
channels: t.channels.map((ch, stemIdx) => {
|
|
249
|
+
const mutes = mutePlans[stemIdx].map((r) => ({
|
|
250
|
+
startBar: Math.max(trimStart, r.startBar),
|
|
251
|
+
endBar: Math.min(trimEnd, r.endBar)
|
|
252
|
+
})).filter((r) => r.endBar > r.startBar);
|
|
253
|
+
return {
|
|
254
|
+
...ch,
|
|
255
|
+
timelineStartBar: tlStart,
|
|
256
|
+
trimStartBar: trimStart,
|
|
257
|
+
trimEndBar: trimEnd,
|
|
258
|
+
volume: stemVolumes[stemIdx],
|
|
259
|
+
// Always keep the lane visible — a "loaded but
|
|
260
|
+
// silent" placeholder still appears on the timeline
|
|
261
|
+
// (rendered as a fully-muted block) so the user can
|
|
262
|
+
// see which tracks are queued for the next session.
|
|
263
|
+
visible: true,
|
|
264
|
+
muteRegions: mutes
|
|
265
|
+
};
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
})
|
|
269
|
+
}));
|
|
270
|
+
for (let stemIdx = 0; stemIdx < 4; stemIdx++) {
|
|
271
|
+
controller.syncMuteRegions(trackId, stemIdx);
|
|
272
|
+
}
|
|
273
|
+
if (clip.transposeSemitones && Math.abs(clip.transposeSemitones) > 0.01) {
|
|
274
|
+
controller.setTrackTranspose(trackId, clip.transposeSemitones);
|
|
275
|
+
}
|
|
276
|
+
controller.syncGainsForTrack(trackId);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { importStemFmMixConfig, isStemFmMixConfig, planClipsFromStemFmConfig };
|
|
280
|
+
//# sourceMappingURL=chunk-U44X6QP5.js.map
|
|
281
|
+
//# sourceMappingURL=chunk-U44X6QP5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daw/engine/daw-import-stem-fm-config.ts"],"names":[],"mappings":"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","file":"chunk-U44X6QP5.js","sourcesContent":["/**\n * Import a stem.fm mix-config JSON into the DAW.\n *\n * The mix-config is a flat list of *sessions* — each one a \"scene\" on the\n * master timeline of length `length.bars`. The model is:\n *\n * • `trackChannels[]` — which tracks are present in this scene (canonical).\n * • `originAnchors[]` — parallel array; where each track starts in its\n * source for this scene (originalBarIndex / sample).\n * • modifier events — `volume` (per-stem), `rate`, `pitch`. They describe\n * how the present tracks are *played* within the scene\n * (per-bar tempo + pitch + per-stem volume automation).\n * • effect events — `reverb` (and friends). Currently ignored —\n * they don't affect clip placement, only sound.\n *\n * High-level mapping to the DAW (initial implementation):\n *\n * ONE clip per (session, trackChannel) pair, length = session.length.bars,\n * source position = anchor.originalBarIndex.\n *\n * Per-stem channel volume + mute regions are derived from the session's\n * volume events. A track that's \"loaded but silent\" (all volumes 0) still\n * gets a clip on the timeline — it just renders muted, ready to come alive\n * in a later session.\n *\n * Rate/pitch automation within a session is collapsed: a single average\n * rate per clip and the first pitch event's value as a constant transpose.\n * This is the right trade-off until the DAW grows per-section variable\n * tempo + clip-level volume automation; until then a session imports as\n * a clean structural skeleton, and we layer the richer automation on top.\n *\n * Track ids are resolved once each via `fetchTrackForDAW` — a track that\n * appears in many sessions is fetched once and the same `ITrackForSequence`\n * is reused for every clip referencing it.\n *\n * Public entry point: `importStemFmMixConfig`.\n */\n\nimport type { ITrackForSequence } from '@/stem-audio-engine-v3/interface';\nimport { DAWController } from './daw-controller';\nimport { fetchTrackForDAW } from '../services/track-search-api';\nimport { useDAWSessionStore } from '../store/daw-session-store';\nimport type { LoadProgress } from './daw-track-loader';\n\n/** Stems live in this order in the DAW (0..3). Must match `STEM_TYPES` /\n * `STEM_LABELS` in `interfaces.ts`. */\nconst STEM_NAMES = ['other', 'vocals', 'bass', 'drums'] as const;\ntype StemName = (typeof STEM_NAMES)[number];\n\n// ── JSON shape (mirror of stem.fm's mix-config export) ──────────────\n\n/** Modifier events shape clip playback (rate / pitch / per-stem volume).\n * Effect events (`reverb`, …) come as a separate `category: 'effect'`\n * shape — see `StemFmEffectEvent`. We currently parse modifiers only. */\nexport type StemFmModifierType = 'rate' | 'pitch' | 'volume';\nexport type StemFmCurve = 'instant' | 'linear';\n\nexport interface StemFmTriggerOrigin {\n /** 0-based bar index in the original (source) audio. */\n originalBarIndex: number;\n /** Wall-clock position of the bar in the source audio (seconds). */\n originalStartTimeSec: number;\n /** Sample position of the bar in the source audio (48 kHz). */\n originalSampleStart: number;\n}\n\n/**\n * Modifier event (rate / pitch / volume). Carries `startValue` / `endValue`\n * in `behavior` — for `instant` events these are equal; for `linear` events\n * they describe a ramp of length `schedule.length.bars` master bars.\n */\nexport interface StemFmModifierEvent {\n category: 'modifier';\n type: StemFmModifierType;\n schedule: {\n /** `bar` is 1-based in the session timeline. */\n triggerTime: { bar: number; position: string };\n /** Ramp length. `bars: 0` ⇒ instant. */\n length: { bars: number; offset: string };\n triggerOrigin?: StemFmTriggerOrigin;\n };\n target: {\n trackId: string;\n /** Volume events target a specific stem; rate / pitch events\n * target the whole track and omit this field. */\n stem?: StemName;\n };\n behavior: {\n startValue: number;\n endValue: number;\n curve: StemFmCurve;\n signatureLabel?: string | null;\n };\n}\n\n/**\n * Effect events (e.g. reverb) carry an entirely different `behavior` shape\n * (`startEnabled / endEnabled / startMix / endMix / startDecay / endDecay`)\n * and are not yet wired through to the DAW's `StemEffectChain`. We type\n * them loosely here so they can flow through the validator without\n * crashing the importer; consumers should ignore unknown event types.\n */\nexport interface StemFmEffectEvent {\n category: 'effect';\n type: string; // 'reverb' | 'delay' | …\n schedule: StemFmModifierEvent['schedule'];\n target: { trackId: string; stem?: StemName };\n behavior: Record<string, unknown>;\n}\n\nexport type StemFmEvent = StemFmModifierEvent | StemFmEffectEvent;\n\nexport interface StemFmSession {\n type: 'playback' | 'transition';\n trackChannels: { id: string }[];\n length: { bars: number; offset: string };\n events: StemFmEvent[];\n /** Convenience duplicate of `length.bars` (newer exports include this). */\n bars?: number;\n /**\n * Where each track in `trackChannels` starts in its source audio for\n * this session. Same length & order as `trackChannels`. Newer exports\n * include this; older ones don't, and we fall back to the first rate\n * event's `triggerOrigin` for the track.\n */\n originAnchors?: StemFmTriggerOrigin[];\n}\n\nexport type StemFmMixConfig = StemFmSession[];\n\n// Type guard: only modifier events have a numeric `startValue`/`endValue`\n// in `behavior`. Used everywhere we need to read those fields.\nfunction isModifierEvent(e: StemFmEvent): e is StemFmModifierEvent {\n return e.category === 'modifier';\n}\n\n// ── Planning types (internal) ───────────────────────────────────────\n\n/**\n * One scheduled clip on the DAW timeline — corresponds 1:1 with a\n * (session, trackChannel) pair from the mix-config.\n */\ninterface PlannedClip {\n /** Which session this clip came from (0-based). For diagnostics. */\n sessionIndex: number;\n /** stem.fm track id (source). */\n sourceTrackId: string;\n /** Where on the global DAW timeline this clip starts. */\n timelineStartBar: number;\n /** Inclusive first source bar (relative to the original track). */\n sourceStartBar: number;\n /** Exclusive last source bar (relative to the original track). */\n sourceEndBar: number;\n /** Per-stem volume to apply on this clip (length 4, [other, vocals, bass, drums]). */\n stemVolumes: [number, number, number, number];\n /** Per-stem mute regions inside the clip (local bars relative to\n * `sourceStartBar`). Each entry shadows a bar range where the\n * stem volume was 0 in the session. */\n stemMuteRegions: [\n Array<{ startBar: number; endBar: number }>,\n Array<{ startBar: number; endBar: number }>,\n Array<{ startBar: number; endBar: number }>,\n Array<{ startBar: number; endBar: number }>,\n ];\n /** Pitch shift in semitones for this clip (constant across the clip). */\n transposeSemitones: number;\n /** Average rate over the clip (informational; the DAW computes a per-bar\n * speed map on its own from `barMapping` vs master tempo). */\n avgRate: number;\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\n/** Modifier events of `type` targeting a `(trackId, stem?)` pair, in bar\n * order. Effect events (e.g. `reverb`) are filtered out — their `behavior`\n * shape is incompatible and the importer doesn't process them yet. */\nfunction modifierEventsForTarget(\n session: StemFmSession,\n type: StemFmModifierType,\n trackId: string,\n stem?: StemName,\n): StemFmModifierEvent[] {\n return session.events\n .filter(isModifierEvent)\n .filter((e) => e.type === type && e.target.trackId === trackId)\n .filter((e) => (stem === undefined ? !e.target.stem : e.target.stem === stem))\n .sort((a, b) => a.schedule.triggerTime.bar - b.schedule.triggerTime.bar);\n}\n\n/**\n * Figure out where a track starts in its source for a given session.\n *\n * Preference order:\n * 1. `session.originAnchors[trackChannelIndex].originalBarIndex` —\n * the explicit anchor stem.fm exports for newer mix-configs. Same\n * length & order as `trackChannels`.\n * 2. The first rate event for that track's `triggerOrigin.originalBarIndex`\n * — fallback for older exports that didn't include `originAnchors`.\n * 3. `0` — last-resort default if neither is available.\n */\nfunction resolveSessionAnchor(\n session: StemFmSession,\n trackChannelIndex: number,\n trackId: string,\n): number {\n const anchors = session.originAnchors;\n if (anchors && trackChannelIndex < anchors.length) {\n return anchors[trackChannelIndex].originalBarIndex;\n }\n const rateEvents = modifierEventsForTarget(session, 'rate', trackId);\n const firstWithOrigin = rateEvents.find((e) => e.schedule.triggerOrigin);\n return firstWithOrigin?.schedule.triggerOrigin?.originalBarIndex ?? 0;\n}\n\n/**\n * Reduce a list of volume events targeting one (track, stem) within a session\n * down to:\n * • a single \"settled\" volume value for the channel, and\n * • a list of bar ranges inside the session where the stem was at zero\n * (which become mute regions on the resulting clip).\n *\n * Walk the events in bar order, holding a piecewise-constant value across\n * the session. The DAW has no clip-level volume automation yet, so:\n *\n * • `instant` events snap to `endValue` at the trigger bar.\n * • `linear` ramps snap to `max(startValue, endValue)` across the ramp\n * window — that way a 0→1 fade-in becomes audible at the start of the\n * ramp (rather than half-mute through the cross-fade) and a 1→0\n * fade-out reads as \"loud through the ramp, then silent\".\n *\n * The returned `channelVolume` is the LARGEST non-zero value across the\n * session, then mute regions carve out where the value is 0.\n */\nfunction reduceVolumeEvents(\n session: StemFmSession,\n trackId: string,\n stem: StemName,\n): {\n channelVolume: number;\n zeroRegionsInSession: Array<{ startBar: number; endBar: number }>;\n} {\n const events = modifierEventsForTarget(session, 'volume', trackId, stem);\n const sessionLen = session.length.bars;\n\n if (events.length === 0) {\n // No volume info → stem.fm runtime defaults to 1 (audible).\n return { channelVolume: 1, zeroRegionsInSession: [] };\n }\n\n const pieces: Array<{ startBar: number; endBar: number; value: number }> = [];\n let cursor = 0;\n let curValue = events[0].behavior.startValue;\n\n for (const ev of events) {\n const evBar = ev.schedule.triggerTime.bar - 1;\n if (evBar > cursor) {\n pieces.push({ startBar: cursor, endBar: evBar, value: curValue });\n cursor = evBar;\n }\n const rampEndBar = evBar + (ev.schedule.length.bars || 0);\n if (rampEndBar > cursor) {\n const rampValue = Math.max(ev.behavior.startValue, ev.behavior.endValue);\n pieces.push({ startBar: cursor, endBar: rampEndBar, value: rampValue });\n cursor = rampEndBar;\n }\n curValue = ev.behavior.endValue;\n }\n if (cursor < sessionLen) {\n pieces.push({ startBar: cursor, endBar: sessionLen, value: curValue });\n }\n\n let channelVolume = 0;\n for (const p of pieces) {\n if (p.value > channelVolume) channelVolume = p.value;\n }\n if (channelVolume === 0) channelVolume = 1; // fully-zero stems get muted via regions below\n\n const zeroRegionsInSession: Array<{ startBar: number; endBar: number }> = [];\n let zeroStart: number | null = null;\n for (const p of pieces) {\n if (p.value <= 0.001) {\n if (zeroStart === null) zeroStart = p.startBar;\n } else if (zeroStart !== null) {\n zeroRegionsInSession.push({ startBar: zeroStart, endBar: p.startBar });\n zeroStart = null;\n }\n }\n if (zeroStart !== null) {\n zeroRegionsInSession.push({ startBar: zeroStart, endBar: sessionLen });\n }\n\n return { channelVolume, zeroRegionsInSession };\n}\n\n/**\n * Average rate (1.0 = unchanged) of all rate events for a track in a\n * session. We don't yet apply per-bar tempo changes from these events;\n * the value is informational and surfaced in the importer log so we can\n * sanity-check tempo intent. Defaults to 1 when there are no rate events.\n */\nfunction averageRateForTrack(session: StemFmSession, trackId: string): number {\n const events = modifierEventsForTarget(session, 'rate', trackId);\n if (events.length === 0) return 1;\n let sum = 0;\n for (const e of events) sum += e.behavior.startValue || 1;\n return sum / events.length;\n}\n\n/**\n * Convert session-relative zero ranges into clip-local source-bar ranges.\n * For the initial implementation a clip's source span is taken to be\n * `[anchor, anchor + session.length.bars)` — i.e. 1:1 bar mapping at the\n * DAW's master tempo, ignoring rate ramps.\n */\nfunction zeroRegionsToMuteRegions(\n zeroRegions: Array<{ startBar: number; endBar: number }>,\n sourceStartBar: number,\n sessionLen: number,\n): Array<{ startBar: number; endBar: number }> {\n if (sessionLen <= 0) return [];\n return zeroRegions\n .map((z) => ({\n startBar: sourceStartBar + Math.max(0, z.startBar),\n endBar: sourceStartBar + Math.min(sessionLen, z.endBar),\n }))\n .filter((r) => r.endBar > r.startBar);\n}\n\n/**\n * Build the full list of planned DAW clips from a mix-config.\n *\n * Initial-implementation rule: emit ONE clip per (session, trackChannel)\n * pair. Source position comes from `originAnchors` (or the first rate\n * event's `triggerOrigin` as a fallback). Length is `session.length.bars`.\n * Per-stem volumes & mute regions come from the session's volume events.\n *\n * No run-splitting on rate-event jumps, no skipping silent placeholders —\n * a track that's \"loaded silent\" appears on the timeline as a muted clip,\n * ready to come alive in a later session.\n *\n * Pure / synchronous so it can be unit-tested without the audio engine.\n */\nexport function planClipsFromStemFmConfig(config: StemFmMixConfig): {\n clips: PlannedClip[];\n totalTimelineBars: number;\n trackIds: string[];\n} {\n const clips: PlannedClip[] = [];\n const seenTrackIds = new Set<string>();\n let timelineCursor = 0;\n\n for (let sessionIdx = 0; sessionIdx < config.length; sessionIdx++) {\n const session = config[sessionIdx];\n const sessionStartBar = timelineCursor;\n const sessionLen = session.bars ?? session.length.bars;\n\n for (let tcIdx = 0; tcIdx < session.trackChannels.length; tcIdx++) {\n const trackId = session.trackChannels[tcIdx].id;\n seenTrackIds.add(trackId);\n\n // 1) Anchor — where this track starts in its source for the session.\n const sourceStartBar = resolveSessionAnchor(session, tcIdx, trackId);\n const sourceEndBar = sourceStartBar + Math.max(1, sessionLen);\n\n // 2) Per-stem volume reduction → channel volume + zero ranges\n // (in session-bar coordinates).\n const perStem = STEM_NAMES.map((s) => reduceVolumeEvents(session, trackId, s));\n const channelVolumes: [number, number, number, number] = [\n perStem[0].channelVolume,\n perStem[1].channelVolume,\n perStem[2].channelVolume,\n perStem[3].channelVolume,\n ];\n const stemMuteRegions: PlannedClip['stemMuteRegions'] = [\n zeroRegionsToMuteRegions(perStem[0].zeroRegionsInSession, sourceStartBar, sessionLen),\n zeroRegionsToMuteRegions(perStem[1].zeroRegionsInSession, sourceStartBar, sessionLen),\n zeroRegionsToMuteRegions(perStem[2].zeroRegionsInSession, sourceStartBar, sessionLen),\n zeroRegionsToMuteRegions(perStem[3].zeroRegionsInSession, sourceStartBar, sessionLen),\n ];\n\n // 3) Constant transpose — first pitch event wins. Pitch events\n // in stem.fm carry semitones in `behavior.startValue`, so\n // this is a 1:1 mapping; ramps are collapsed to the end value.\n const pitchEvents = modifierEventsForTarget(session, 'pitch', trackId);\n const transposeSemitones = pitchEvents.length > 0 ? pitchEvents[0].behavior.endValue : 0;\n\n // The DAW renderer / scheduler computes the timeline position of\n // each visible source bar as `ch.timelineStartBar + sourceBar`\n // (see `computeClips` and `daw-controller.gateStems`). To make\n // a clip sourced from `[sourceStartBar, sourceEndBar)` land at\n // `sessionStartBar` on the timeline we need\n // timelineStartBar = sessionStartBar - sourceStartBar\n // — i.e. a negative offset that cancels the trim-in. This is\n // why `applyClipStateToTrack` bypasses `setStemTimelineStart`\n // (whose drag-UI clamping rejects negatives).\n clips.push({\n sessionIndex: sessionIdx,\n sourceTrackId: trackId,\n timelineStartBar: sessionStartBar - sourceStartBar,\n sourceStartBar,\n sourceEndBar,\n stemVolumes: channelVolumes,\n stemMuteRegions,\n transposeSemitones,\n avgRate: averageRateForTrack(session, trackId),\n });\n }\n\n timelineCursor += sessionLen;\n }\n\n return {\n clips,\n totalTimelineBars: timelineCursor,\n trackIds: Array.from(seenTrackIds),\n };\n}\n\n// ── Public API ─────────────────────────────────────────────────────\n\nexport interface ImportStemFmCallbacks {\n onStatus?: (message: string) => void;\n onPercent?: (percent: number) => void;\n /**\n * Limit the import to the first N sessions of the mix-config. Useful\n * while the DAW doesn't yet de-duplicate repeated source tracks — a\n * long playback session that jumps backwards in its source track\n * (e.g. session 3 of the test file revisits \"What Goes Around\" 7\n * times) explodes into many independently-decoded clips of the same\n * audio. Defaults to `Infinity` (import everything).\n */\n maxSessions?: number;\n}\n\n/**\n * Validate that a parsed object looks like a stem.fm mix-config (an array of\n * sessions). Throws a `TypeError` with a human-friendly message when the\n * shape doesn't match, so callers can show the error directly.\n */\nexport function isStemFmMixConfig(value: unknown): value is StemFmMixConfig {\n if (!Array.isArray(value) || value.length === 0) return false;\n const first = value[0] as Partial<StemFmSession> | undefined;\n if (!first) return false;\n if (first.type !== 'playback' && first.type !== 'transition') return false;\n if (!Array.isArray(first.trackChannels)) return false;\n if (!first.length || typeof first.length.bars !== 'number') return false;\n if (!Array.isArray(first.events)) return false;\n return true;\n}\n\n/**\n * Import a stem.fm mix-config JSON, replacing the current DAW session.\n *\n * Caller is expected to have already cleared the previous session (matches\n * the `restoreSession` pattern in `daw-session-restore.ts`).\n *\n * Returns the planned clips so the caller can show a summary to the user.\n */\nexport async function importStemFmMixConfig(\n config: StemFmMixConfig,\n callbacks?: ImportStemFmCallbacks,\n): Promise<{\n clips: PlannedClip[];\n totalTimelineBars: number;\n addedTrackInstances: number;\n}> {\n const controller = DAWController.getInstance();\n if (!controller.isReady()) await controller.init();\n\n callbacks?.onStatus?.('Planning clips from stem.fm config…');\n callbacks?.onPercent?.(0);\n\n // Optional truncation. We slice BEFORE planning so the timeline cursor\n // and per-clip start bars all reflect the truncated set — otherwise\n // we'd plan clips for sessions we never actually load.\n const maxSessions = callbacks?.maxSessions ?? Infinity;\n const effectiveConfig = maxSessions < config.length\n ? config.slice(0, maxSessions)\n : config;\n if (effectiveConfig.length < config.length) {\n console.info(\n `[importStemFmMixConfig] Truncating ${config.length} sessions ` +\n `→ first ${effectiveConfig.length} (maxSessions=${maxSessions}).`,\n );\n }\n\n const plan = planClipsFromStemFmConfig(effectiveConfig);\n const { clips, totalTimelineBars, trackIds } = plan;\n\n if (clips.length === 0) {\n throw new Error('stem.fm config produced no audible clips');\n }\n\n // ── Phase 1 (0-15%): pre-fetch every unique track's metadata + beat\n // grid once, so 9 clips of the same source only hit the API one time.\n const trackDataCache = new Map<string, ITrackForSequence>();\n callbacks?.onStatus?.(`Resolving ${trackIds.length} track${trackIds.length === 1 ? '' : 's'}…`);\n for (let i = 0; i < trackIds.length; i++) {\n const id = trackIds[i];\n callbacks?.onPercent?.(Math.round((i / trackIds.length) * 15));\n try {\n const td = await fetchTrackForDAW(id);\n trackDataCache.set(id, td);\n } catch (err) {\n console.warn('[importStemFmMixConfig] failed to fetch track', id, err);\n }\n }\n\n // ── Phase 2 (15-95%): add a DAW track instance per clip and wait for\n // each one's audio to fully decode in the background.\n //\n // `controller.addTrack()` returns as soon as the audio worklets are\n // wired up — the actual fMP4 decode happens asynchronously and emits\n // `trackLoaded` on the controller when every batch has landed. The\n // previous implementation here only awaited the worklet setup, so the\n // import would \"finish\" while waveforms were still filling in — what\n // looked to the user like only ~70% of the song was getting loaded.\n //\n // We now (a) start every `addTrack` sequentially (so worklet creation\n // is well-ordered, and `addTrack`'s own progress callback covers the\n // common case of fully-cached files completing instantly) and then\n // (b) wait on all `trackLoaded` events in parallel before declaring\n // the import done.\n const decodeProgress = new Array(clips.length).fill(0);\n const emitAggregateProgress = () => {\n const avg = decodeProgress.reduce((a, b) => a + b, 0) / decodeProgress.length;\n // Map 0..100 (avg decode) → 15..95 (overall import).\n callbacks?.onPercent?.(15 + Math.round((avg / 100) * 80));\n };\n\n let addedTrackInstances = 0;\n const decodeCompletePromises: Promise<void>[] = [];\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n const trackData = trackDataCache.get(clip.sourceTrackId);\n if (!trackData) {\n decodeProgress[i] = 100;\n emitAggregateProgress();\n continue;\n }\n\n callbacks?.onStatus?.(\n `Loading clip ${i + 1}/${clips.length} from \"${trackData.name || clip.sourceTrackId}\"…`,\n );\n\n const idx = i;\n const onProgress = (p: LoadProgress) => {\n decodeProgress[idx] = p.percent;\n emitAggregateProgress();\n if (p.detail) callbacks?.onStatus?.(`${trackData.name}: ${p.detail}`);\n };\n\n const dawTrack = await controller.addTrack(trackData, onProgress);\n if (!dawTrack) {\n decodeProgress[idx] = 100;\n emitAggregateProgress();\n continue;\n }\n addedTrackInstances++;\n\n applyClipStateToTrack(controller, dawTrack.id, dawTrack.barMapping.length, clip);\n\n // Wait for THIS track's decode to finish. `whenTrackLoaded`\n // resolves immediately when the source is already fully decoded\n // — the common case under the controller's source-cache when\n // multiple clips reference the same source URL. Wrapping in a\n // single Promise.race with a timeout protects against the\n // (rare) silent-failure path where the decode pipeline errors\n // out and never resolves. Five minutes is well past a normal\n // full-song decode even over slow network.\n const trackId = dawTrack.id;\n decodeCompletePromises.push(\n Promise.race([\n controller.whenTrackLoaded(trackId).then(() => {\n decodeProgress[idx] = 100;\n emitAggregateProgress();\n }),\n new Promise<void>((resolve) => setTimeout(() => {\n console.warn(\n `[importStemFmMixConfig] decode timeout for track ${trackId} ` +\n `(${trackData.name}); proceeding with partial audio.`,\n );\n decodeProgress[idx] = 100;\n emitAggregateProgress();\n resolve();\n }, 5 * 60 * 1000)),\n ]).catch((err) => {\n // `whenTrackLoaded` rejects if the track is removed\n // mid-import. Treat that as \"done with whatever we got\"\n // so the import doesn't hang on user cancellation.\n console.warn(`[importStemFmMixConfig] track ${trackId} aborted:`, err);\n decodeProgress[idx] = 100;\n emitAggregateProgress();\n }),\n );\n }\n\n callbacks?.onStatus?.(`Decoding ${addedTrackInstances} clip${addedTrackInstances === 1 ? '' : 's'}…`);\n await Promise.all(decodeCompletePromises);\n\n // ── Phase 3 (95-100%): final touches.\n callbacks?.onPercent?.(95);\n callbacks?.onStatus?.('Finalising session…');\n const store = useDAWSessionStore.getState();\n store.setMixName('stem.fm imported mix');\n controller.seekToBar(0);\n\n // Clear undo stack so the user can't accidentally Ctrl+Z back through\n // the import (matches restoreSession behaviour).\n useDAWSessionStore.setState({ _undoStack: [], _redoStack: [] });\n\n callbacks?.onPercent?.(100);\n callbacks?.onStatus?.(\n `Imported ${addedTrackInstances} clip${addedTrackInstances === 1 ? '' : 's'} (${totalTimelineBars} bars).`,\n );\n\n return { clips, totalTimelineBars, addedTrackInstances };\n}\n\n/**\n * Apply a planned clip's trim / volume / mute / transpose state to a freshly\n * added DAW track. Extracted out so the main import flow stays readable.\n *\n * NOTE: we mutate the store state directly here rather than going through\n * `setStemTimelineStart` because that setter is shared with the drag UI and\n * keeps its own clamping rules — see the comment block in\n * `daw-session-store.ts`. Imports legitimately need negative\n * `timelineStartBar` to express \"source bar N appears at timeline bar 0\".\n */\nfunction applyClipStateToTrack(\n controller: DAWController,\n trackId: string,\n totalSourceBars: number,\n clip: PlannedClip,\n): void {\n // Clamp the requested trim window to the actual source length.\n // stem.fm occasionally reports `originalBarIndex` values one bar past\n // the last actual bar (rounding artefact), so we tighten the window\n // to whatever the track really decodes to.\n const trimStart = Math.max(0, Math.min(totalSourceBars - 1, clip.sourceStartBar));\n const trimEnd = Math.max(trimStart + 1, Math.min(totalSourceBars, clip.sourceEndBar));\n const tlStart = Math.round(clip.timelineStartBar);\n const stemVolumes = clip.stemVolumes;\n const mutePlans = clip.stemMuteRegions;\n\n useDAWSessionStore.setState((state) => ({\n tracks: state.tracks.map((t) => {\n if (t.id !== trackId) return t;\n return {\n ...t,\n channels: t.channels.map((ch, stemIdx) => {\n const mutes = mutePlans[stemIdx]\n .map((r) => ({\n startBar: Math.max(trimStart, r.startBar),\n endBar: Math.min(trimEnd, r.endBar),\n }))\n .filter((r) => r.endBar > r.startBar);\n return {\n ...ch,\n timelineStartBar: tlStart,\n trimStartBar: trimStart,\n trimEndBar: trimEnd,\n volume: stemVolumes[stemIdx],\n // Always keep the lane visible — a \"loaded but\n // silent\" placeholder still appears on the timeline\n // (rendered as a fully-muted block) so the user can\n // see which tracks are queued for the next session.\n visible: true,\n muteRegions: mutes,\n };\n }),\n };\n }),\n }));\n\n for (let stemIdx = 0; stemIdx < 4; stemIdx++) {\n controller.syncMuteRegions(trackId, stemIdx);\n }\n\n if (clip.transposeSemitones && Math.abs(clip.transposeSemitones) > 0.01) {\n controller.setTrackTranspose(trackId, clip.transposeSemitones);\n }\n\n controller.syncGainsForTrack(trackId);\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
// src/daw/store/daw-auth-store.ts
|
|
5
|
+
var useDAWAuthStore = create()(
|
|
6
|
+
persist(
|
|
7
|
+
(set) => ({
|
|
8
|
+
ndaAccepted: false,
|
|
9
|
+
acceptNDA: () => {
|
|
10
|
+
set({ ndaAccepted: true });
|
|
11
|
+
},
|
|
12
|
+
logout: () => {
|
|
13
|
+
set({ ndaAccepted: false });
|
|
14
|
+
}
|
|
15
|
+
}),
|
|
16
|
+
{
|
|
17
|
+
name: "stemstudio-auth",
|
|
18
|
+
partialize: (state) => ({
|
|
19
|
+
ndaAccepted: state.ndaAccepted
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export { useDAWAuthStore };
|
|
26
|
+
//# sourceMappingURL=chunk-UKMELGZL.js.map
|
|
27
|
+
//# sourceMappingURL=chunk-UKMELGZL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daw/store/daw-auth-store.ts"],"names":[],"mappings":";;;;AAgBO,IAAM,kBAAkB,MAAqB,EAAA;AAAA,EAChD,OAAA;AAAA,IACI,CAAC,GAAS,MAAA;AAAA,MACN,WAAa,EAAA,KAAA;AAAA,MAEb,WAAW,MAAM;AACb,QAAI,GAAA,CAAA,EAAE,WAAa,EAAA,IAAA,EAAM,CAAA,CAAA;AAAA,OAC7B;AAAA,MAEA,QAAQ,MAAM;AACV,QAAI,GAAA,CAAA,EAAE,WAAa,EAAA,KAAA,EAAO,CAAA,CAAA;AAAA,OAC9B;AAAA,KACJ,CAAA;AAAA,IACA;AAAA,MACI,IAAM,EAAA,iBAAA;AAAA,MACN,UAAA,EAAY,CAAC,KAAW,MAAA;AAAA,QACpB,aAAa,KAAM,CAAA,WAAA;AAAA,OACvB,CAAA;AAAA,KACJ;AAAA,GACJ;AACJ","file":"chunk-UKMELGZL.js","sourcesContent":["/**\n * DAW Auth Store — NDA gate state.\n *\n * Tracks whether the user has accepted the beta NDA.\n * Persisted to localStorage so they only see the gate once per browser.\n */\n\nimport { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\n\ninterface DAWAuthState {\n ndaAccepted: boolean;\n acceptNDA: () => void;\n logout: () => void;\n}\n\nexport const useDAWAuthStore = create<DAWAuthState>()(\n persist(\n (set) => ({\n ndaAccepted: false,\n\n acceptNDA: () => {\n set({ ndaAccepted: true });\n },\n\n logout: () => {\n set({ ndaAccepted: false });\n },\n }),\n {\n name: 'stemstudio-auth',\n partialize: (state) => ({\n ndaAccepted: state.ndaAccepted,\n }),\n }\n )\n);\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DAWView – Main DAW interface.
|
|
5
|
+
* Layout: [Left icon bar] [Suggestions panel?] [Timeline] [Clip inspector?]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface DAWViewProps {
|
|
9
|
+
/**
|
|
10
|
+
* Optional content rendered at the far-right of the merged
|
|
11
|
+
* transport bar (account dropdown, what's-new pill, etc.). Forwarded
|
|
12
|
+
* verbatim to `DAWTransportBar` — DAWView itself stays agnostic
|
|
13
|
+
* about what those bits are.
|
|
14
|
+
*/
|
|
15
|
+
rightExtras?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
declare const DAWView: React.FC<DAWViewProps>;
|
|
18
|
+
|
|
19
|
+
export { DAWView };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { DAWView } from '../chunk-LO74ZJ4H.js';
|
|
2
|
+
import '../chunk-QQ5NZTHT.js';
|
|
3
|
+
import '../chunk-OFGZURP6.js';
|
|
4
|
+
import '../chunk-AAVC7KUW.js';
|
|
5
|
+
import '../chunk-U44X6QP5.js';
|
|
6
|
+
import '../chunk-OYNES5W3.js';
|
|
7
|
+
import '../chunk-56PWIP7O.js';
|
|
8
|
+
import '../chunk-TBXCZFAY.js';
|
|
9
|
+
import '../chunk-KCOOE2OP.js';
|
|
10
|
+
//# sourceMappingURL=DAWView.js.map
|
|
11
|
+
//# sourceMappingURL=DAWView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"DAWView.js"}
|