Skip to content

Commit 9aafee6

Browse files
Merge pull request #42 from gantasmo/feat/mix-overhaul
Feat/mix overhaul
2 parents 964d4a7 + 4cddff0 commit 9aafee6

7 files changed

Lines changed: 361 additions & 39 deletions

File tree

docs/reports/feature-doc-coverage-report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Feature Documentation Coverage Report
22

33
> [!NOTE]
4-
> Generated: 2026-06-04T00:03:28.008Z · Git revision: `97c1f25331f1` · Repomix tracked: **no**
4+
> Generated: 2026-06-04T00:59:56.920Z · Git revision: `558142ceb17c` · Repomix tracked: **no**
55
66
## Audit Dashboard
77

docs/reports/feature-doc-coverage.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"generatedAt": "2026-06-04T00:03:28.008Z",
3-
"repoRevision": "97c1f25331f1",
2+
"generatedAt": "2026-06-04T00:59:56.920Z",
3+
"repoRevision": "558142ceb17c",
44
"repomixContext": {
55
"path": "repomix-output.md",
66
"present": false,

docs/screenshots/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generatedAt": "2026-06-04T00:05:28.550Z",
2+
"generatedAt": "2026-06-04T01:02:35.334Z",
33
"entries": [
44
{
55
"file": "01-shell-make.png",

frontend/src/state/djEngine.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ let cueBus: GainNode | null = null;
130130
let cueDest: MediaStreamAudioDestinationNode | null = null;
131131
let cueAudioEl: HTMLAudioElement | null = null;
132132
let cueSinkId = '';
133+
// Sampler bank (D7): one-shot pads routed through djMaster (so they ride the DJ
134+
// mix + limiter + visualizer). Decoded buffers keyed by pad id.
135+
const samples = new Map<string, AudioBuffer>();
136+
let samplerGain: GainNode | null = null;
133137
const decks: Partial<Record<DeckId, Deck>> = {};
134138
let crossfade = 0; // -1 = full A, 0 = center, +1 = full B
135139
let rafId = 0;
@@ -169,6 +173,13 @@ function ensureCueBus(): GainNode {
169173
return cueBus;
170174
}
171175

176+
function ensureSamplerGain(): GainNode {
177+
if (samplerGain) return samplerGain;
178+
samplerGain = getEngineCtx().createGain();
179+
samplerGain.connect(ensureMaster());
180+
return samplerGain;
181+
}
182+
172183
/** The node a playing source feeds into: the key-lock pitch insert when engaged,
173184
* else the deck's delay-comp input (insert bypassed). */
174185
function deckInputNode(d: Deck): AudioNode {
@@ -710,6 +721,32 @@ export function getCueSinkId(): string {
710721
return cueSinkId;
711722
}
712723

724+
/* -------------------------------- sampler bank (D7) ------------------------ */
725+
726+
/** Load a one-shot sample into a pad (decode the URL to a buffer). */
727+
export async function loadSample(padId: string, url: string): Promise<void> {
728+
const ctx = getEngineCtx();
729+
const r = await fetch(url);
730+
if (!r.ok) throw new Error(`sample fetch ${r.status}`);
731+
samples.set(padId, await ctx.decodeAudioData(await r.arrayBuffer()));
732+
}
733+
734+
/** Fire a pad's sample as a one-shot through the DJ master (polyphonic). */
735+
export function triggerSample(padId: string): void {
736+
const buf = samples.get(padId);
737+
if (!buf) return;
738+
const ctx = getEngineCtx();
739+
if (ctx.state === 'suspended') void ctx.resume().catch(() => { /* retry next gesture */ });
740+
const s = ctx.createBufferSource();
741+
s.buffer = buf;
742+
s.connect(ensureSamplerGain());
743+
s.onended = () => { try { s.disconnect(); } catch { /* gone */ } };
744+
s.start();
745+
}
746+
747+
export function clearSample(padId: string): void { samples.delete(padId); }
748+
export function hasSample(padId: string): boolean { return samples.has(padId); }
749+
713750
/** Crossfader position in [-1, 1] (equal-power). */
714751
export function setCrossfade(x: number): void {
715752
crossfade = clamp(x, -1, 1);
@@ -885,6 +922,8 @@ export function dispose(): void {
885922
if (cueBus) { try { cueBus.disconnect(); } catch { /* gone */ } cueBus = null; }
886923
if (cueAudioEl) { try { cueAudioEl.pause(); cueAudioEl.srcObject = null; } catch { /* gone */ } cueAudioEl = null; }
887924
cueDest = null;
925+
if (samplerGain) { try { samplerGain.disconnect(); } catch { /* gone */ } samplerGain = null; }
926+
samples.clear();
888927
if (djMaster) { try { djMaster.disconnect(); } catch { /* gone */ } djMaster = null; }
889928
listeners.clear();
890929
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { create } from 'zustand';
2+
import { persist } from 'zustand/middleware';
3+
4+
/* DJ sampler bank (D7) — persisted pad → library-entry assignments. The decoded
5+
* one-shot buffers live in djEngine (re-decoded on mount from each entry's
6+
* audioUrl); this store only remembers which track sits on which pad so the
7+
* bank survives a reload. */
8+
9+
export interface SamplerPad {
10+
entryId: string;
11+
name: string;
12+
}
13+
14+
interface DjSamplerState {
15+
pads: Record<number, SamplerPad>;
16+
setPad: (i: number, pad: SamplerPad) => void;
17+
clearPad: (i: number) => void;
18+
}
19+
20+
export const useDjSampler = create<DjSamplerState>()(
21+
persist(
22+
(set) => ({
23+
pads: {},
24+
setPad: (i, pad) => set((s) => ({ pads: { ...s.pads, [i]: pad } })),
25+
clearPad: (i) => set((s) => {
26+
const p = { ...s.pads };
27+
delete p[i];
28+
return { pads: p };
29+
}),
30+
}),
31+
{ name: 'thedaw.dj.sampler.v1' },
32+
),
33+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { create } from 'zustand';
2+
import { persist } from 'zustand/middleware';
3+
4+
/* DJ side list (D7) — an ephemeral "prepare / play-next" staging queue, distinct
5+
* from the persisted named Setlists. The DJ drags tracks here from the browser
6+
* while a set is playing, reorders them into the order they want to hear them,
7+
* then fires each onto a deck (→A/→B) or pushes the whole queue into the active
8+
* Automix set. Only the library-entry id + a display label are stored (the
9+
* browser resolves BPM/key/art live), so a staged queue survives a reload and
10+
* silently drops tracks that no longer exist in the library. */
11+
12+
export interface SideListItem {
13+
entryId: string;
14+
label: string;
15+
}
16+
17+
interface DjSideListState {
18+
items: SideListItem[];
19+
/** Append a track to the end of the queue; no-ops if already staged. */
20+
add: (item: SideListItem) => void;
21+
/** Remove a staged track by entry id. */
22+
remove: (entryId: string) => void;
23+
/** Move the item at `from` to index `to` (clamped). */
24+
reorder: (from: number, to: number) => void;
25+
/** Empty the queue. */
26+
clear: () => void;
27+
}
28+
29+
export const useDjSideList = create<DjSideListState>()(
30+
persist(
31+
(set) => ({
32+
items: [],
33+
add: (item) => set((s) => (
34+
s.items.some((it) => it.entryId === item.entryId)
35+
? s
36+
: { items: [...s.items, item] }
37+
)),
38+
remove: (entryId) => set((s) => ({ items: s.items.filter((it) => it.entryId !== entryId) })),
39+
reorder: (from, to) => set((s) => {
40+
if (to < 0 || to >= s.items.length || from === to) return s;
41+
const arr = [...s.items];
42+
const [it] = arr.splice(from, 1);
43+
arr.splice(to, 0, it);
44+
return { items: arr };
45+
}),
46+
clear: () => set({ items: [] }),
47+
}),
48+
{ name: 'thedaw.dj.sidelist.v1' },
49+
),
50+
);

0 commit comments

Comments
 (0)