A continuation of index.html. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓2,059 lines · 88.3 KB
osc.stop(now + 0.1);
}
};
if (drumSynth[type]) drumSynth[type]();
}
startVisualizer() {
const waveCanvas = document.getElementById('waveformCanvas');
const specCanvas = document.getElementById('spectrumCanvas');
const waveCtx = waveCanvas.getContext('2d');
const specCtx = specCanvas.getContext('2d');
const resize = () => {
waveCanvas.width = waveCanvas.offsetWidth * 2;
waveCanvas.height = waveCanvas.offsetHeight * 2;
specCanvas.width = specCanvas.offsetWidth * 2;
specCanvas.height = specCanvas.offsetHeight * 2;
};
resize();
window.addEventListener('resize', resize);
const bufferLength = this.analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const freqArray = new Uint8Array(bufferLength);
const draw = () => {
requestAnimationFrame(draw);
this.analyser.getByteTimeDomainData(dataArray);
waveCtx.fillStyle = '#0a0a0a';
waveCtx.fillRect(0, 0, waveCanvas.width, waveCanvas.height);
waveCtx.lineWidth = 2;
waveCtx.strokeStyle = '#4ade80';
waveCtx.beginPath();
const sliceWidth = waveCanvas.width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * waveCanvas.height / 2;
if (i === 0) waveCtx.moveTo(x, y);
else waveCtx.lineTo(x, y);
x += sliceWidth;
}
waveCtx.stroke();
this.analyser.getByteFrequencyData(freqArray);
specCtx.fillStyle = '#0a0a0a';
specCtx.fillRect(0, 0, specCanvas.width, specCanvas.height);
const barWidth = specCanvas.width / 64;
let barX = 0;
for (let i = 0; i < 64; i++) {
const idx = Math.floor(i * bufferLength / 64);
const barHeight = (freqArray[idx] / 255) * specCanvas.height;
const hue = (i / 64) * 60 + 100;
specCtx.fillStyle = `hsl(${hue}, 70%, 50%)`;
specCtx.fillRect(barX, specCanvas.height - barHeight, barWidth - 2, barHeight);
barX += barWidth;
}
const level = Math.max(...freqArray) / 255 * 100;
document.getElementById('masterL').style.height = level + '%';
document.getElementById('masterR').style.height = (level * 0.9 + Math.random() * 10) + '%';
// Update channel meters
document.querySelectorAll('.channel-strip').forEach((strip, idx) => {
const meters = strip.querySelectorAll('.vu-level');
const chLevel = level * (0.7 + Math.random() * 0.3);
meters.forEach(m => m.style.height = chLevel + '%');
});
};
draw();
}
}
const audio = new AudioEngine();
// State
let isPlaying = false;
let isRecording = false;
let tempo = 120;
let currentStep = 0;
let sequencerInterval = null;
let startTime = null;
let elapsedTime = 0;
let timerInterval = null;
const drums = [
{ id: 'kick', label: 'Kick' },
{ id: 'snare', label: 'Snare' },
{ id: 'hihat', label: 'Hi-Hat' },
{ id: 'openhat', label: 'Open Hat' },
{ id: 'clap', label: 'Clap' },
{ id: 'tom', label: 'Tom' },
{ id: 'rim', label: 'Rim' },
{ id: 'perc', label: 'Perc' }
];
const pattern = {};
drums.forEach(d => { pattern[d.id] = new Array(16).fill(false); });
const patterns = {
basic: { kick: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0], snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], hihat: [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0] },
hiphop: { kick: [1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0], snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], hihat: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], openhat: [0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0] },
house: { kick: [1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0], clap: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], hihat: [0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0], openhat: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0] },
dnb: { kick: [1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0], snare: [0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0], hihat: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] },
trap: { kick: [1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0], snare: [0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0], hihat: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], clap: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0] }
};
// Build UI
function buildInstrumentList() {
const lists = { synth: [], keyboard: [], basslead: [] };
Object.entries(audio.instruments).forEach(([id, inst]) => {
lists[inst.category].push({ id, label: inst.label });
});
['synth', 'keyboard', 'basslead'].forEach((cat, catIdx) => {
const containerId = cat === 'basslead' ? 'bassLeadList' : cat + 'List';
const icons = ['SY', 'KB', 'BL'];
document.getElementById(containerId).innerHTML = lists[cat].map(i =>
`<div class="instrument-item ${audio.currentInstrument === i.id ? 'active' : ''}" onclick="selectInstrument('${i.id}')">
<div class="inst-icon">${icons[catIdx]}</div>
<span class="inst-name">${i.label}</span>
</div>`
).join('');
});
}
function selectInstrument(id) {
audio.currentInstrument = id;
const inst = audio.instruments[id];
document.getElementById('attackSlider').value = inst.attack * 1000;
document.getElementById('decaySlider').value = inst.decay * 1000;
document.getElementById('sustainSlider').value = inst.sustain * 100;
document.getElementById('releaseSlider').value = inst.release * 1000;
updateADSR();
buildInstrumentList();
}
function buildKeyboard() {
const keyboard = document.getElementById('keyboard');
const whiteNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
const blackNotes = ['C#', 'D#', 'F#', 'G#', 'A#'];
let html = '<div class="white-keys">';
for (let oct = 0; oct < 3; oct++) {
whiteNotes.forEach(note => {
const octave = audio.octave + oct;
html += `<div class="key white" data-note="${note}" data-octave="${octave}"><span class="key-label">${note}${octave}</span></div>`;
});
}
html += '</div>';
keyboard.innerHTML = html;
const blackPositions = [0, 1, 3, 4, 5];
let blackHtml = '';
for (let oct = 0; oct < 3; oct++) {
blackPositions.forEach((pos, idx) => {
const note = blackNotes[idx];
const octave = audio.octave + oct;
const left = oct * 7 * 38 + pos * 38 + 26;
blackHtml += `<div class="key black" data-note="${note}" data-octave="${octave}" style="left: ${left}px"><span class="key-label">${note}</span></div>`;
});
}
keyboard.innerHTML += blackHtml;
document.querySelectorAll('.key').forEach(key => {
key.addEventListener('mousedown', () => {
audio.playNote(key.dataset.note, parseInt(key.dataset.octave));
key.classList.add('active');
});
key.addEventListener('mouseup', () => {
audio.stopNote(key.dataset.note, parseInt(key.dataset.octave));
key.classList.remove('active');
});
key.addEventListener('mouseleave', () => {
audio.stopNote(key.dataset.note, parseInt(key.dataset.octave));
key.classList.remove('active');
});
});
}
function buildSequencer() {
const seq = document.getElementById('sequencer');
let html = '<div class="seq-header">';
for (let i = 1; i <= 16; i++) {
html += `<span class="step-num ${i % 4 === 1 ? 'beat' : ''}">${i}</span>`;
}
html += '</div>';
drums.forEach(drum => {
html += `<div class="seq-row">
<span class="drum-label" onclick="audio.playDrum('${drum.id}')">${drum.label}</span>
<div class="seq-steps">`;
for (let i = 0; i < 16; i++) {
html += `<div class="seq-step ${i % 4 === 0 ? 'beat-marker' : ''} ${pattern[drum.id][i] ? 'active' : ''}" data-drum="${drum.id}" data-step="${i}" onclick="toggleStep('${drum.id}', ${i})"></div>`;
}
html += '</div></div>';
});
seq.innerHTML = html;
}
function buildChannelStrips() {
const strips = document.getElementById('channelStrips');
const channels = [
{ id: 'synth', name: 'Synth' },
{ id: 'drums', name: 'Drums' },
{ id: 'fx', name: 'FX' }
];
strips.innerHTML = channels.map(ch => `
<div class="channel-strip" data-channel="${ch.id}">
<div class="channel-name">${ch.name}</div>
<div class="knob-container">
<div class="knob" id="pan-${ch.id}"></div>
<span class="knob-label">Pan</span>
<span class="knob-value" id="pan-val-${ch.id}">C</span>
</div>
<div class="meter-container">
<div class="vu-meter"><div class="vu-level" style="height: 0%"></div></div>
<div class="vu-meter"><div class="vu-level" style="height: 0%"></div></div>
</div>
<div class="fader-container">
<div class="fader-track" id="fader-track-${ch.id}">
<div class="fader-fill" id="fader-fill-${ch.id}"></div>
<div class="fader-handle" id="fader-handle-${ch.id}"></div>
</div>
<span class="fader-db" id="fader-db-${ch.id}">0.0 dB</span>
</div>
<div class="channel-btns">
<button class="ch-btn mute" data-channel="${ch.id}" onclick="toggleMute('${ch.id}', this)">M</button>
<button class="ch-btn solo" data-channel="${ch.id}" onclick="toggleSolo('${ch.id}', this)">S</button>
</div>
</div>
`).join('');
// Attach knobs and faders
channels.forEach(ch => {
const panKnob = document.getElementById(`pan-${ch.id}`);
knobController.attach(panKnob, {
value: 50, min: 0, max: 100, default: 50,
onChange: (val) => {
audio.setChannelPan(ch.id, val);
const panVal = document.getElementById(`pan-val-${ch.id}`);
if (val < 48) panVal.textContent = 'L' + Math.round(50 - val);
else if (val > 52) panVal.textContent = 'R' + Math.round(val - 50);
else panVal.textContent = 'C';
}
});
const track = document.getElementById(`fader-track-${ch.id}`);
const handle = document.getElementById(`fader-handle-${ch.id}`);
const fill = document.getElementById(`fader-fill-${ch.id}`);
const dbDisplay = document.getElementById(`fader-db-${ch.id}`);
faderController.attach(track, handle, fill, dbDisplay, {
value: ch.id === 'fx' ? 60 : 80,
onChange: (val) => audio.setChannelGain(ch.id, val)
});
});
}
function buildEffects() {
const fxGrid = document.getElementById('fxGrid');
const effects = [
{ id: 'reverb', name: 'Reverb', enabled: true, params: [
{ id: 'mix', label: 'Mix', value: 30, min: 0, max: 100 },
{ id: 'decay', label: 'Decay', value: 50, min: 0, max: 100 }
]},
{ id: 'delay', name: 'Delay', enabled: false, params: [
{ id: 'time', label: 'Time', value: 25, min: 0, max: 100 },
{ id: 'feedback', label: 'Fdbk', value: 30, min: 0, max: 100 }
]},
{ id: 'chorus', name: 'Chorus', enabled: false, params: [
{ id: 'rate', label: 'Rate', value: 50, min: 0, max: 100 },
{ id: 'depth', label: 'Depth', value: 30, min: 0, max: 100 }
]},
{ id: 'distortion', name: 'Distort', enabled: false, params: [
{ id: 'amount', label: 'Drive', value: 30, min: 0, max: 100 },
{ id: 'tone', label: 'Tone', value: 50, min: 0, max: 100 }
]}
];
fxGrid.innerHTML = effects.map(fx => `
<div class="fx-module ${fx.enabled ? 'active' : ''}" id="fx-module-${fx.id}">
<div class="fx-header">
<span class="fx-name">${fx.name}</span>
<div class="fx-toggle ${fx.enabled ? 'active' : ''}" id="fx-toggle-${fx.id}" onclick="toggleFx('${fx.id}')"></div>
</div>
<div class="fx-knobs">
${fx.params.map(p => `
<div class="fx-knob-wrap">
<div class="mini-knob" id="fx-knob-${fx.id}-${p.id}"></div>
<span class="mini-knob-label">${p.label}</span>
</div>
`).join('')}
</div>
</div>
`).join('');
// Attach effect knobs
effects.forEach(fx => {
fx.params.forEach(p => {
const knob = document.getElementById(`fx-knob-${fx.id}-${p.id}`);
knobController.attach(knob, {
value: p.value, min: p.min, max: p.max, default: p.value,
onChange: (val) => {
let scaledVal = val / 100;
if (fx.id === 'delay' && p.id === 'time') scaledVal = val / 100;
audio.setEffectParam(fx.id, p.id, scaledVal);
}
});
});
});
}
function toggleMute(channel, btn) {
btn.classList.toggle('active');
audio.setChannelMute(channel, btn.classList.contains('active'));
}
function toggleSolo(channel, btn) {
btn.classList.toggle('active');
audio.setChannelSolo(channel, btn.classList.contains('active'));
}
function toggleFx(id) {
const toggle = document.getElementById(`fx-toggle-${id}`);
const module = document.getElementById(`fx-module-${id}`);
toggle.classList.toggle('active');
module.classList.toggle('active');
audio.toggleEffect(id, toggle.classList.contains('active'));
}
function toggleStep(drum, step) {
pattern[drum][step] = !pattern[drum][step];
const el = document.querySelector(`[data-drum="${drum}"][data-step="${step}"]`);
el.classList.toggle('active');
}
function togglePlay() {
isPlaying = !isPlaying;
document.getElementById('playBtn').textContent = isPlaying ? '⏸' : '▶';
if (isPlaying) {
startTime = Date.now() - elapsedTime;
timerInterval = setInterval(updateTimer, 100);
playSequencer();
} else {
clearInterval(timerInterval);
clearInterval(sequencerInterval);
}
}
function stopAll() {
isPlaying = false;
document.getElementById('playBtn').textContent = '▶';
clearInterval(timerInterval);
clearInterval(sequencerInterval);
currentStep = 0;
elapsedTime = 0;
document.getElementById('timeDisplay').textContent = '00:00:00';
highlightStep(-1);
}
function playSequencer() {
const stepTime = (60 / tempo / 4) * 1000;
sequencerInterval = setInterval(() => {
drums.forEach(drum => {
if (pattern[drum.id][currentStep]) audio.playDrum(drum.id);
});
highlightStep(currentStep);
currentStep = (currentStep + 1) % 16;
}, stepTime);
}
function highlightStep(step) {
document.querySelectorAll('.seq-step').forEach(el => el.classList.remove('playing'));
if (step >= 0) {
document.querySelectorAll(`[data-step="${step}"]`).forEach(el => el.classList.add('playing'));
}
}
function updateTimer() {
elapsedTime = Date.now() - startTime;
const totalSeconds = Math.floor(elapsedTime / 1000);
const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
document.getElementById('timeDisplay').textContent = `${hours}:${minutes}:${seconds}`;
}
function toggleRecord() {
isRecording = !isRecording;
document.getElementById('recBtn').classList.toggle('active', isRecording);
}
function rewind() { currentStep = 0; elapsedTime = 0; startTime = Date.now(); document.getElementById('timeDisplay').textContent = '00:00:00'; }
function forward() { currentStep = (currentStep + 4) % 16; }
function changeTempo(delta) {
tempo = Math.max(40, Math.min(300, tempo + delta));
document.getElementById('tempoValue').textContent = tempo;
if (isPlaying) { clearInterval(sequencerInterval); playSequencer(); }
}
function changeOctave(delta) {
audio.octave = Math.max(1, Math.min(6, audio.octave + delta));
document.getElementById('octaveDisplay').textContent = audio.octave;
buildKeyboard();
}
function updateADSR() {
const attack = document.getElementById('attackSlider').value;
const decay = document.getElementById('decaySlider').value;
const sustain = document.getElementById('sustainSlider').value;
const release = document.getElementById('releaseSlider').value;
document.getElementById('attackVal').textContent = attack + 'ms';
document.getElementById('decayVal').textContent = decay + 'ms';
document.getElementById('sustainVal').textContent = sustain + '%';
document.getElementById('releaseVal').textContent = release + 'ms';
const inst = audio.instruments[audio.currentInstrument];
inst.attack = attack / 1000;
inst.decay = decay / 1000;
inst.sustain = sustain / 100;
inst.release = release / 1000;
}
function updateFilter() {
audio.filterCutoff = document.getElementById('filterSlider').value;
audio.filterQ = document.getElementById('resSlider').value;
document.getElementById('filterVal').textContent = audio.filterCutoff + 'Hz';
document.getElementById('resVal').textContent = parseFloat(audio.filterQ).toFixed(1);
}
function updateMasterVol() {
const vol = document.getElementById('masterVol').value;
if (audio.masterGain) audio.masterGain.gain.value = vol / 100;
const db = vol === 0 ? '-∞' : (20 * Math.log10(vol / 80)).toFixed(1);
document.getElementById('masterDb').textContent = db === '-∞' ? '-∞ dB' : db + ' dB';
}
function clearPattern() {
drums.forEach(d => { pattern[d.id] = new Array(16).fill(false); });
buildSequencer();
}
function loadPattern(name) {
clearPattern();
const p = patterns[name];
if (p) {
Object.entries(p).forEach(([drum, steps]) => {
steps.forEach((val, idx) => { pattern[drum][idx] = val === 1; });
});
}
buildSequencer();
}
function showTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
document.getElementById(tabId + 'Panel').classList.add('active');
}
// Arpeggiator
let arpRunning = false;
let arpInterval = null;
const arpRates = ['1/1', '1/2', '1/4', '1/8', '1/16'];
function updateArpRate() { document.getElementById('arpRateVal').textContent = arpRates[document.getElementById('arpRate').value]; }
function updateArpOctaves() { document.getElementById('arpOctVal').textContent = document.getElementById('arpOctaves').value; }
function toggleArpeggiator() {
arpRunning = !arpRunning;
document.getElementById('arpToggle').textContent = arpRunning ? 'Stop Arp' : 'Start Arp';
if (arpRunning) startArpeggiator();
else clearInterval(arpInterval);
}
function startArpeggiator() {
const rates = [2000, 1000, 500, 250, 125];
const rate = rates[document.getElementById('arpRate').value];
const octaves = parseInt(document.getElementById('arpOctaves').value);
const patternType = document.getElementById('arpPattern').value;
const notes = ['C', 'E', 'G', 'B'];
let noteIdx = 0, octOffset = 0, direction = 1;
arpInterval = setInterval(() => {
audio.playNote(notes[noteIdx], audio.octave + octOffset);
setTimeout(() => audio.stopNote(notes[noteIdx], audio.octave + octOffset), rate * 0.8);
if (patternType === 'up') {
noteIdx++;
if (noteIdx >= notes.length) { noteIdx = 0; octOffset = (octOffset + 1) % octaves; }
} else if (patternType === 'down') {
noteIdx--;
if (noteIdx < 0) { noteIdx = notes.length - 1; octOffset = (octOffset - 1 + octaves) % octaves; }
} else if (patternType === 'updown') {
noteIdx += direction;
if (noteIdx >= notes.length || noteIdx < 0) { direction *= -1; noteIdx += direction * 2; }
} else if (patternType === 'random') {
noteIdx = Math.floor(Math.random() * notes.length);
octOffset = Math.floor(Math.random() * octaves);
}
}, rate);
}
// Keyboard shortcuts
const keyMap = { 'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E', 'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A', 'u': 'A#', 'j': 'B', 'k': 'C', 'o': 'C#', 'l': 'D' };
const activeKeys = new Set();
document.addEventListener('keydown', e => {
if (e.repeat) return;
const note = keyMap[e.key.toLowerCase()];
if (note && !activeKeys.has(e.key)) {
activeKeys.add(e.key);
const octave = ['k', 'o', 'l'].includes(e.key.toLowerCase()) ? audio.octave + 1 : audio.octave;
audio.playNote(note, octave);
const key = document.querySelector(`.key[data-note="${note}"][data-octave="${octave}"]`);
if (key) key.classList.add('active');
}
if (e.key === ' ') { e.preventDefault(); togglePlay(); }
if (e.key === 'z') changeOctave(-1);
if (e.key === 'x') changeOctave(1);
});
document.addEventListener('keyup', e => {
const note = keyMap[e.key.toLowerCase()];
if (note) {
activeKeys.delete(e.key);
const octave = ['k', 'o', 'l'].includes(e.key.toLowerCase()) ? audio.octave + 1 : audio.octave;
audio.stopNote(note, octave);
const key = document.querySelector(`.key[data-note="${note}"][data-octave="${octave}"]`);
if (key) key.classList.remove('active');
}
});
// Initialize
buildInstrumentList();
buildKeyboard();
buildSequencer();
buildChannelStrips();
buildEffects();
updateArpRate();
updateArpOctaves();
updateMasterVol();
document.addEventListener('click', () => audio.init(), { once: true });
</script>
</body>
</html>