To add a new track to the mixer, you must update the worker configuration, add stem definitions, and upload audio files.
In everything-is-remixed-worker.js, add an entry to the TRACKS object:
const TRACKS = {
// ... existing tracks ...
'newtrack': {
name: 'NewTrack',
bpm: 128,
key: 'A Major',
number: 8,
symbol: 'Nt',
color: '#00ff00'
},
};In stems.json, add an entry for the new track:
{
"newtrack": [
{
"file": "1-Kick.m4a",
"name": "KICK",
"desc": "Kick drum pattern",
"color": "#00ff00",
"downSample": true,
"mono": true
},
{
"file": "2-Bass.m4a",
"name": "BASS",
"desc": "Bass line",
"color": "#00cc00",
"downSample": true,
"mono": true
}
// ... more stems
]
}Stem Properties:
file: Filename in R2 bucketname: Display name (uppercase)desc: Description (optional)color: Hex color for channeldownSample: Optimization flag (for bass-heavy stems)mono: Optimization flag (for non-stereo stems)
Pre-generate waveform data to avoid audio decoding on load:
- Open
src/peak-generator.htmlin a browser - Select the new track from the dropdown
- Click "Generate Peaks" to process all stems
- Download the generated JSON file
- Save as
src/workers/newtrack_peaks.json
R2 Bucket:
- Create a new R2 bucket in Cloudflare dashboard
- Name it consistently (e.g.,
newtrack-stems) - Upload all stem audio files (
.m4aformat)
Worker Binding:
Add an R2 bucket binding for the new track (e.g., NEWTRACK → newtrack-stems).
The FX system can be extended with new effect types.
In app/modules/mixer-constants.js, add the new parameter to DEFAULT_FX_STATE:
export const DEFAULT_FX_STATE = {
eq: { low: 0, mid: 0, high: 0 },
filter: { freq: 20000, resonance: 1, type: 'lowpass' },
reverb: 0,
delay: { time: 0.375, feedback: 0.3, mix: 0 },
pan: 0,
// Add new effect:
chorus: { rate: 1, depth: 0, mix: 0 }
};In app/modules/mixer-audio.js, add a method to create the effect nodes:
createChorus() {
// Create LFO for modulation
const lfo = this.ctx.createOscillator();
const lfoGain = this.ctx.createGain();
const delay = this.ctx.createDelay(0.05);
const wet = this.ctx.createGain();
const dry = this.ctx.createGain();
const merger = this.ctx.createChannelMerger(2);
lfo.frequency.value = 1;
lfoGain.gain.value = 0;
delay.delayTime.value = 0.025;
wet.gain.value = 0;
dry.gain.value = 1;
lfo.connect(lfoGain);
lfoGain.connect(delay.delayTime);
lfo.start();
return { lfo, lfoGain, delay, wet, dry, merger };
}In app/modules/mixer-templates.js, add the control section to the FX modal template (in the appropriate tab):
<div class="fx-section">
<label>Chorus</label>
<div class="fx-control">
<span class="fx-label">Rate</span>
<input type="range" min="0.1" max="10" step="0.1" value="${fx.chorus.rate}"
class="fx-slider" id="chorus-rate-${index}">
<span class="fx-value" id="chorus-rate-val-${index}">${fx.chorus.rate.toFixed(1)}Hz</span>
</div>
<div class="fx-control">
<span class="fx-label">Depth</span>
<input type="range" min="0" max="100" step="1" value="${fx.chorus.depth}"
class="fx-slider" id="chorus-depth-${index}">
<span class="fx-value" id="chorus-depth-val-${index}">${Math.round(fx.chorus.depth)}%</span>
</div>
<div class="fx-control">
<span class="fx-label">Mix</span>
<input type="range" min="0" max="100" step="1" value="${fx.chorus.mix}"
class="fx-slider" id="chorus-mix-${index}">
<span class="fx-value" id="chorus-mix-val-${index}">${Math.round(fx.chorus.mix)}%</span>
</div>
</div>In app/modules/mixer-fx.js, add handlers in setupModalListeners():
modal.querySelector(`#chorus-rate-${index}`).addEventListener('input', e => {
const value = parseFloat(e.target.value);
this.state.updateFX(index, 'chorus', 'rate', value);
player.effects.chorus.lfo.frequency.setTargetAtTime(value, currentTime(), 0.01);
modal.querySelector(`#chorus-rate-val-${index}`).textContent = `${value.toFixed(1)}Hz`;
if (this.onUpdate) this.onUpdate();
});
// ... depth and mix handlersIn app/modules/mixer-state.js, extend toShareUrl() and applyFromUrl() to include the new parameters.
Source → EQ → [Compressor] → [Distortion] → Filter → [Ring Mod] → Delay → [Tremolo] → Panner → Gain → Analyser → Master
Nodes in brackets are lazily instantiated — only created when the user first interacts with their controls.
New effects follow the lazy instantiation pattern used by existing effects:
- Create the factory in
app/modules/mixer-audio.js(returns an object withinput,output,connect, and parameter setters) - Add an
_ensure*()method inapp/modules/mixer-fx.jsthat disconnects adjacent nodes and splices the new effect into the chain - Add defaults to
DEFAULT_FX_STATEinmixer-constants.js - Add controls in
mixer-templates.js - Wire up listeners in
mixer-fx.js_setupModalListeners() - Add apply/reset logic in
applyToNode()andresetNode() - Extend URL encoding in
mixer-state.jstoShareUrl()andapplyFromUrl()
Example: See _ensureCompressor(), _ensureDistortion(), _ensureTremolo(), or _ensureRingMod() for the splice pattern.
In app/mixer-style.css, modify the .channel class:
.channel {
width: 80px; /* Default: 60px */
min-width: 80px;
}.fader {
height: 150px; /* Default: 100px */
}The mixer uses CSS custom properties for theming:
:root {
--track-color: #4ecdc4; /* Injected by worker */
}
.channel-btn.active {
background: var(--track-color);
box-shadow: 0 0 10px var(--track-color);
}Theme is controlled via data-theme attribute on the document root:
:root[data-theme="dark"] {
--bg: #0a0a0a;
--fg: #ffffff;
}
:root[data-theme="light"] {
--bg: #f0f0f0;
--fg: #1a1a1a;
}Theme Toggle:
const newTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('evr-theme', newTheme);Early Loading (prevents flash):
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('evr-theme') || 'dark');</script>Theme-Aware Components:
- Listing grid (Swiss Lab cards)
- Start overlay
- Loading view
- Mixer panel
- Channel buttons
- Waveforms (stem + master/holograph)
- Help modal icons
- Reduce FFT Size:
export const FFT_SIZE = { mobile: 32, desktop: 64 };-
Use Pre-generated Peaks:
- Ensure all
{trackId}_peaks.jsonfiles exist - Avoids storing full AudioBuffers
- Ensure all
-
Reduce Batch Size:
export const BATCH_SIZE = { mobile: 2, desktop: 5 };The mixer automatically detects mobile devices:
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)
|| ('ontouchstart' in window);Mobile gets reduced settings:
- Smaller FFT sizes
- Smaller batch sizes
- Touch-optimized fader handling
No Sound:
- Check browser console for AudioContext state
- Ensure user interaction before
play() - Verify stem URLs are accessible
Clicking/Popping:
- Use
setTargetAtTime()instead of direct value assignment - Check for rapid parameter changes
Safari Issues:
- Always call
audioContext.resume()on play - Check for webkit-specific AudioContext
Modal Not Appearing:
- Verify modal element exists in DOM
- Check click event is reaching the FX button
Values Not Applying:
- Check event listener is attached
- Verify audio node references
- Check state is being updated
Tabs Not Switching:
- Verify tab button click handlers
- Check
.activeclass is toggling
Help Modal Not Appearing:
- Verify
HelpController.init()was called - Check help button exists (only on track pages)
- Verify modal elements in DOM
Keyboard Shortcut (?) Not Working:
- Check focus isn't in an input/textarea/select
- Verify keydown listener is attached
- Check
isInputFocused()method
Mobile Bottom Sheet Not Swiping:
- Verify
isMobiledetection is correct - Check touch event listeners on handle element
- Verify
bindSwipeEvents()was called
URL Not Working:
- Check parameter count matches expected
- Verify scaling factors (×10, ×100)
- Test with minimal state first
- Use browser DevTools Performance tab
- Check
requestAnimationFramecallback duration - Reduce FFT sizes or disable meters
Before deploying, verify:
- All stems load correctly
- Waveforms display
- Play/pause/stop work
- Skip buttons work (±10s)
- Restart button works
- Mute/solo toggle correctly
- FX modal opens/closes (click FX button)
- FX modal all 4 tabs switch correctly (EQ/FILTER, DYNAMICS, MOD/FX, SEND/DELAY)
- FX sliders affect audio
- Compressor controls affect audio (lazy instantiation)
- Distortion drive/tone/mix work (lazy instantiation)
- Tremolo rate/depth/shape work (lazy instantiation)
- Ring Modulator freq/shape/mix work (lazy instantiation)
- Filter rolloff swap works with ring mod in chain
- Pan control works
- Share URL generates
- Share URL loads correctly
- Theme toggle works (listing, loading, mixer views)
- Theme persists across page reloads
- No flash of wrong theme on load
- Waveforms update on theme change
- Holograph visualizer updates on theme change
- Mobile touch works
- Master fader controls volume
- Reset button works
- Signal LEDs light up when audio plays
- Help button opens help modal
- Help modal tabs switch correctly
- Help modal closes (×, backdrop, Escape, Got it)
- Help keyboard shortcut (?) works
- Mobile: Help bottom sheet swipe-to-dismiss works
- Holograph visualizer animates during playback