MCPcopy Create free account
hub / github.com/stemdeckapp/stemdeck / createChunkedAudioEngine

Function createChunkedAudioEngine

static/js/chunkedAudioEngine.js:116–442  ·  view source on GitHub ↗
(stems, { onTime, onEnded, context } = {})

Source from the content-addressed store, hash-verified

114 * @param {{onTime?:(t:number)=>void, onEnded?:()=>void, context?:AudioContext}} opts
115 */
116export function createChunkedAudioEngine(stems, { onTime, onEnded, context } = {}) {
117 const AC = window.AudioContext || window.webkitAudioContext;
118 const ctx = context || new AC();
119 const ownsCtx = !context;
120 const master = ctx.createGain();
121
122 let stNode = null;
123 let _playbackRate = 1.0;
124 const _workletReady = (ctx.audioWorklet
125 ? ctx.audioWorklet.addModule('/vendor/soundtouch-processor.js').then(() => {
126 stNode = new AudioWorkletNode(ctx, 'soundtouch-processor');
127 master.connect(stNode);
128 stNode.connect(ctx.destination);
129 }).catch((err) => {
130 console.warn('[chunkedEngine] SoundTouch worklet failed, tape-effect fallback:', err);
131 master.connect(ctx.destination);
132 })
133 : Promise.resolve().then(() => { master.connect(ctx.destination); }));
134
135 // Per-stem state: url, parsed WAV header, gain node, currently playing nodes
136 const stemMap = new Map();
137 for (const s of stems) {
138 if (!s?.url) continue;
139 const gain = ctx.createGain();
140 gain.connect(master);
141 stemMap.set(s.name, { url: s.url, header: null, gain, activeNodes: [] });
142 }
143
144 let _duration = 0;
145 let playing = false;
146 let destroyed = false;
147 let rafId = null;
148
149 // Playback clock: getCurrentTime = ctx.currentTime - _startCtxTime + _startOffset
150 let _startCtxTime = 0;
151 let _startOffset = 0;
152 // _scheduledTo: track position (seconds) up to which AudioBufferSourceNodes
153 // have already been scheduled. Always sits at a chunk boundary after play().
154 let _scheduledTo = 0;
155 // True once the first AudioBufferSourceNode is actually queued; guards
156 // getCurrentTime() from advancing during an async chunk fetch.
157 let _audioStarted = false;
158 let _filling = false; // prevents concurrent _scheduleNext() calls
159
160 // Chunk cache: chunkIdx -> { promise: Promise<Map>, result: Map|null }
161 // result is set synchronously once the promise resolves so play() can
162 // schedule chunk 0 without an async await after ready() completes.
163 const _cache = new Map();
164
165 function _getCurrentTime() {
166 if (!playing || !_audioStarted) return _startOffset;
167 return Math.min((ctx.currentTime - _startCtxTime) * _playbackRate + _startOffset, _duration);
168 }
169
170 // --- fetch helpers ---
171
172 async function _fetchHeader(url) {
173 const res = await fetch(url, { headers: { Range: "bytes=0-1023" } });

Callers 1

openTrackFunction · 0.90

Calls 2

_fetchHeaderFunction · 0.85
_fetchChunkFunction · 0.85

Tested by

no test coverage detected