@@ -130,6 +130,10 @@ let cueBus: GainNode | null = null;
130130let cueDest : MediaStreamAudioDestinationNode | null = null ;
131131let cueAudioEl : HTMLAudioElement | null = null ;
132132let 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 ;
133137const decks : Partial < Record < DeckId , Deck > > = { } ;
134138let crossfade = 0 ; // -1 = full A, 0 = center, +1 = full B
135139let 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). */
174185function 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). */
714751export 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}
0 commit comments