modular-66 / index.html
fluxthedev's picture
Add 2 files
1aea5a5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analog Synth Emulator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&display=swap');
:root {
--knob-size: 50px;
--panel-color: #111827;
--accent-color: #8b5cf6;
--highlight-color: #c4b5fd;
}
body {
font-family: 'Major Mono Display', monospace;
background-color: #1f2937;
color: white;
overflow-x: hidden;
}
.synth-panel {
background: linear-gradient(145deg, #111827, #1e293b);
border: 1px solid #374151;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
}
.knob {
width: var(--knob-size);
height: var(--knob-size);
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #4b5563, #1f2937 75%);
border: 2px solid #374151;
position: relative;
cursor: pointer;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
.knob::after {
content: '';
position: absolute;
top: 5px;
left: 50%;
width: 3px;
height: 15px;
background-color: var(--highlight-color);
transform-origin: bottom center;
transform: translateX(-50%) rotate(0deg);
border-radius: 3px;
}
.button {
width: var(--knob-size);
height: var(--knob-size);
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #4b5563, #1f2937 75%);
border: 2px solid #374151;
cursor: pointer;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s;
}
.button.active {
background: radial-gradient(circle at 30% 30%, var(--accent-color), #1f2937 75%);
box-shadow: 0 0 10px var(--highlight-color);
}
.slider {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: linear-gradient(90deg, var(--accent-color), #4b5563);
border-radius: 3px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--highlight-color);
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
}
.key {
flex: 1;
height: 120px;
border: 1px solid #111827;
position: relative;
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: 10px;
cursor: pointer;
transition: all 0.1s;
user-select: none;
}
.key.white {
background: linear-gradient(180deg, #f9fafb, #d1d5db);
color: #111827;
z-index: 1;
box-shadow: inset 0 -5px 10px rgba(0, 0, 0, 0.2);
}
.key.black {
background: linear-gradient(180deg, #111827, #4b5563);
width: 60%;
height: 80px;
margin-left: -15%;
margin-right: -15%;
z-index: 2;
color: white;
}
.key:hover {
opacity: 0.9;
}
.key.white.active {
background: linear-gradient(180deg, #d1d5db, #9ca3af);
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
}
.key.black.active {
background: linear-gradient(180deg, #4b5563, #111827);
box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.1);
}
.patch-cable {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.oscilloscope {
width: 100%;
height: 100px;
background-color: #111827;
border: 1px solid var(--accent-color);
position: relative;
overflow: hidden;
}
.waveform {
position: absolute;
bottom: 50%;
left: 0;
width: 100%;
height: 2px;
background-color: var(--highlight-color);
transform-origin: left center;
}
.lfo-indicator {
position: absolute;
top: 50%;
left: 0;
width: 10px;
height: 10px;
background-color: #f59e0b;
border-radius: 50%;
transform: translate(-5px, -5px);
z-index: 10;
}
.cable-port {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #111827;
border: 2px solid var(--accent-color);
cursor: pointer;
position: relative;
z-index: 5;
}
.cable-port.active {
background-color: var(--highlight-color);
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.blink {
animation: blink 1s infinite;
}
</style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center p-4">
<div class="w-full max-w-5xl">
<div class="synth-panel rounded-xl p-6 mb-8">
<h1 class="text-3xl font-bold mb-2 text-center text-violet-400">MODULAR 66</h1>
<p class="text-xs text-gray-400 text-center mb-6">ANALOGUE MONOPHONIC SYNTHESIZER</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- VCO Section -->
<div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
<h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
<i class="fas fa-wave-square mr-2"></i> VCO
</h2>
<div class="grid grid-cols-3 gap-4">
<div class="flex flex-col items-center">
<label class="text-xs mb-1">WAVE</label>
<div class="flex flex-col space-y-2">
<div class="button" data-wave="sine" title="Sine">
<i class="fas fa-wave-sine"></i>
</div>
<div class="button" data-wave="square" title="Square">
<i class="fas fa-wave-square"></i>
</div>
<div class="button" data-wave="sawtooth" title="Sawtooth">
<i class="fas fa-wave-triangle"></i>
</div>
</div>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">OCT</label>
<div class="knob" id="octave-knob"></div>
<span class="text-xs mt-1" id="octave-value">0</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">DETUNE</label>
<div class="knob" id="detune-knob"></div>
<span class="text-xs mt-1" id="detune-value">0</span>
</div>
</div>
</div>
<!-- VCF Section -->
<div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
<h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
<i class="fas fa-filter mr-2"></i> VCF
</h2>
<div class="grid grid-cols-3 gap-4">
<div class="flex flex-col items-center">
<label class="text-xs mb-1">CUTOFF</label>
<div class="knob" id="cutoff-knob"></div>
<span class="text-xs mt-1" id="cutoff-value">1000</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">RES</label>
<div class="knob" id="resonance-knob"></div>
<span class="text-xs mt-1" id="resonance-value">0.5</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">ENV</label>
<div class="knob" id="filter-env-knob"></div>
<span class="text-xs mt-1" id="filter-env-value">0.5</span>
</div>
</div>
</div>
<!-- ENV Section -->
<div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
<h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
<i class="fas fa-project-diagram mr-2"></i> ENVELOPE
</h2>
<div class="grid grid-cols-4 gap-2">
<div class="flex flex-col items-center">
<label class="text-xs mb-1">A</label>
<div class="knob small" id="attack-knob"></div>
<span class="text-xs mt-1" id="attack-value">0.1</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">D</label>
<div class="knob small" id="decay-knob"></div>
<span class="text-xs mt-1" id="decay-value">0.3</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">S</label>
<div class="knob small" id="sustain-knob"></div>
<span class="text-xs mt-1" id="sustain-value">0.5</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">R</label>
<div class="knob small" id="release-knob"></div>
<span class="text-xs mt-1" id="release-value">1.0</span>
</div>
</div>
</div>
</div>
<!-- LFO Section -->
<div class="mt-6 bg-gray-900/50 p-4 rounded-lg border border-gray-700">
<h2 class="text-lg mb-3 text-violet-300 border-b border-gray-700 pb-2 flex items-center">
<i class="fas fa-asterisk mr-2 blink"></i> LFO
</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="flex flex-col items-center">
<label class="text-xs mb-1">WAVE</label>
<div class="flex space-x-2">
<div class="button" data-lfo="sine" title="Sine">
<i class="fas fa-wave-sine"></i>
</div>
<div class="button" data-lfo="square" title="Square">
<i class="fas fa-wave-square"></i>
</div>
</div>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">RATE</label>
<div class="knob" id="lfo-rate-knob"></div>
<span class="text-xs mt-1" id="lfo-rate-value">1.0</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">AMOUNT</label>
<div class="knob" id="lfo-amount-knob"></div>
<span class="text-xs mt-1" id="lfo-amount-value">0.5</span>
</div>
<div class="flex flex-col items-center">
<label class="text-xs mb-1">TARGET</label>
<select id="lfo-target" class="bg-gray-800 text-white text-xs p-1 rounded border border-gray-700 w-full">
<option value="cutoff">Cutoff</option>
<option value="detune">Detune</option>
</select>
</div>
</div>
</div>
<!-- Oscilloscope Display -->
<div class="mt-6 flex flex-col">
<div class="oscilloscope rounded-t-lg" id="oscilloscope">
<div class="waveform"></div>
<div class="lfo-indicator"></div>
</div>
<div class="bg-gray-900/50 p-2 rounded-b-lg border border-gray-700 border-t-0 flex justify-between">
<span class="text-xs text-violet-300">SIGNAL OUTPUT</span>
<div class="flex items-center space-x-2">
<div class="cable-port" data-target="vcf"></div>
<div class="cable-port" data-target="lfo"></div>
</div>
</div>
</div>
<!-- Keyboard -->
<div class="mt-6 flex h-32 bg-gray-900 rounded-b-lg">
<div class="key white" data-note="C"><span>C</span></div>
<div class="key black" data-note="C#"></div>
<div class="key white" data-note="D"><span>D</span></div>
<div class="key black" data-note="D#"></div>
<div class="key white" data-note="E"><span>E</span></div>
<div class="key white" data-note="F"><span>F</span></div>
<div class="key black" data-note="F#"></div>
<div class="key white" data-note="G"><span>G</span></div>
<div class="key black" data-note="G#"></div>
<div class="key white" data-note="A"><span>A</span></div>
<div class="key black" data-note="A#"></div>
<div class="key white" data-note="B"><span>B</span></div>
<div class="key white" data-note="C" data-octave="1"><span>C</span></div>
</div>
</div>
</div>
<div class="text-xs text-gray-400 mt-4">
MODULAR 66 ANALOG SYNTHESIZER EMULATOR • USE KEYBOARD WITH QWERTY: A,S,D...E,R...U,I,O
</div>
<svg id="patch-cable" class="patch-cable">
<path stroke="var(--accent-color)" stroke-width="2" fill="none" />
</svg>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Tone.js
const synth = new Tone.MonoSynth({
oscillator: {
type: "sine"
},
envelope: {
attack: 0.1,
decay: 0.3,
sustain: 0.5,
release: 1
},
filter: {
Q: 1,
type: "lowpass",
rolloff: -12
},
filterEnvelope: {
attack: 0.001,
decay: 0.2,
sustain: 0.5,
release: 0.2,
baseFrequency: 300,
octaves: 4
}
}).toDestination();
// LFO
const lfo = new Tone.LFO({
type: "sine",
frequency: 1,
min: 0,
max: 1
}).start();
// Set up keyboard mapping
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': 'C1'
};
// Current state
let currentWave = "sine";
let currentLfoWave = "sine";
let lfoTarget = "cutoff";
let activeKeys = {};
let draggingCable = false;
let cableStart = null;
// DOM elements
const keys = document.querySelectorAll('.key');
const waveButtons = document.querySelectorAll('[data-wave]');
const lfoButtons = document.querySelectorAll('[data-lfo]');
const cablePorts = document.querySelectorAll('.cable-port');
const patchCable = document.querySelector('#patch-cable path');
// Knob elements and their ranges
const knobs = {
'octave': { element: document.getElementById('octave-knob'), valueElement: document.getElementById('octave-value'), min: -2, max: 2, step: 1, value: 0 },
'detune': { element: document.getElementById('detune-knob'), valueElement: document.getElementById('detune-value'), min: -1200, max: 1200, step: 10, value: 0 },
'cutoff': { element: document.getElementById('cutoff-knob'), valueElement: document.getElementById('cutoff-value'), min: 20, max: 20000, value: 1000 },
'resonance': { element: document.getElementById('resonance-knob'), valueElement: document.getElementById('resonance-value'), min: 0.1, max: 10, value: 1 },
'filter-env': { element: document.getElementById('filter-env-knob'), valueElement: document.getElementById('filter-env-value'), min: 0, max: 1, value: 0.5 },
'attack': { element: document.getElementById('attack-knob'), valueElement: document.getElementById('attack-value'), min: 0.001, max: 2, value: 0.1 },
'decay': { element: document.getElementById('decay-knob'), valueElement: document.getElementById('decay-value'), min: 0.001, max: 2, value: 0.3 },
'sustain': { element: document.getElementById('sustain-knob'), valueElement: document.getElementById('sustain-value'), min: 0, max: 1, value: 0.5 },
'release': { element: document.getElementById('release-knob'), valueElement: document.getElementById('release-value'), min: 0.001, max: 4, value: 1 },
'lfo-rate': { element: document.getElementById('lfo-rate-knob'), valueElement: document.getElementById('lfo-rate-value'), min: 0.1, max: 10, value: 1 },
'lfo-amount': { element: document.getElementById('lfo-amount-knob'), valueElement: document.getElementById('lfo-amount-value'), min: 0, max: 1, value: 0.5 }
};
// Initialize knobs
Object.keys(knobs).forEach(key => {
const knob = knobs[key];
let rotation = 0;
let isDragging = false;
let startY = 0;
knob.element.addEventListener('mousedown', (e) => {
isDragging = true;
startY = e.clientY;
knob.element.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaY = startY - e.clientY;
const sensitivity = key === 'octave' ? 50 : 100;
rotation += deltaY / sensitivity;
// Limit rotation to -150 to 150 degrees
rotation = Math.max(-150, Math.min(150, rotation));
// Calculate value based on rotation
const normalized = (rotation + 150) / 300; // 0 to 1
const value = knob.min + (normalized * (knob.max - knob.min));
// For octave knob, round to nearest step
const finalValue = knob.step ?
Math.round(value / knob.step) * knob.step :
Number(value.toFixed(3));
knob.value = finalValue;
updateKnob(knob.element, rotation);
updateSynthParameter(key, finalValue);
startY = e.clientY;
});
document.addEventListener('mouseup', () => {
isDragging = false;
knob.element.style.cursor = 'pointer';
});
});
// Update knob visual rotation
function updateKnob(element, rotation) {
element.style.transform = `rotate(${rotation}deg)`;
}
// Update synth parameters based on knob values
function updateSynthParameter(param, value) {
switch(param) {
case 'octave':
knobs.octave.valueElement.textContent = value;
break;
case 'detune':
synth.oscillator.detune = value;
knobs.detune.valueElement.textContent = value;
break;
case 'cutoff':
synth.filter.frequency.value = value;
knobs.cutoff.valueElement.textContent = Math.round(value);
break;
case 'resonance':
synth.filter.Q.value = value;
knobs.resonance.valueElement.textContent = value.toFixed(1);
break;
case 'filter-env':
synth.filterEnvelope.baseFrequency = 300 + (value * 4000);
knobs['filter-env'].valueElement.textContent = value.toFixed(2);
break;
case 'attack':
synth.envelope.attack = value;
knobs.attack.valueElement.textContent = value.toFixed(3);
break;
case 'decay':
synth.envelope.decay = value;
knobs.decay.valueElement.textContent = value.toFixed(3);
break;
case 'sustain':
synth.envelope.sustain = value;
knobs.sustain.valueElement.textContent = value.toFixed(2);
break;
case 'release':
synth.envelope.release = value;
knobs.release.valueElement.textContent = value.toFixed(3);
break;
case 'lfo-rate':
lfo.frequency.value = value;
knobs['lfo-rate'].valueElement.textContent = value.toFixed(1);
break;
case 'lfo-amount':
lfo.max = value;
knobs['lfo-amount'].valueElement.textContent = value.toFixed(2);
break;
}
}
// Waveform selection
waveButtons.forEach(button => {
button.addEventListener('click', () => {
waveButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentWave = button.dataset.wave;
synth.oscillator.type = currentWave;
});
});
// LFO controls
lfoButtons.forEach(button => {
button.addEventListener('click', () => {
lfoButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentLfoWave = button.dataset.lfo;
lfo.type = currentLfoWave;
});
});
document.getElementById('lfo-target').addEventListener('change', (e) => {
lfoTarget = e.target.value;
// Disconnect previous target
lfo.disconnect();
// Connect to new target
if (lfoTarget === 'cutoff') {
lfo.connect(synth.filter.frequency);
} else if (lfoTarget === 'detune') {
lfo.connect(synth.oscillator.detune);
}
});
// Keyboard interaction
keys.forEach(key => {
key.addEventListener('mousedown', () => playNote(key.dataset.note));
key.addEventListener('mouseup', () => releaseNote(key.dataset.note));
key.addEventListener('mouseleave', () => {
if (key.classList.contains('active')) {
releaseNote(key.dataset.note);
}
});
});
// Computer keyboard interaction
document.addEventListener('keydown', (e) => {
if (keyMap[e.key] && !activeKeys[e.key]) {
activeKeys[e.key] = true;
const note = keyMap[e.key];
const keyElement = document.querySelector(`.key[data-note="${note}"]`);
if (keyElement) {
keyElement.classList.add('active');
playNote(note);
}
}
});
document.addEventListener('keyup', (e) => {
if (keyMap[e.key]) {
const note = keyMap[e.key];
const keyElement = document.querySelector(`.key[data-note="${note}"]`);
if (keyElement) {
keyElement.classList.remove('active');
releaseNote(note);
}
delete activeKeys[e.key];
}
});
// Play a note
function playNote(note) {
let octave = 4;
if (note.endsWith('1')) {
note = note.replace('1', '');
octave = 5;
}
synth.triggerAttack(`${note}${octave}`);
// Visual feedback for active note
const activeNoteIndicator = document.createElement('div');
activeNoteIndicator.className = 'absolute top-0 left-0 w-full h-full bg-violet-400/20 pointer-events-none';
document.querySelector(`.key[data-note="${note}"${note.endsWith('1') ? ' data-octave="1"' : ''}]`)
.appendChild(activeNoteIndicator);
setTimeout(() => {
activeNoteIndicator.style.opacity = '0';
setTimeout(() => activeNoteIndicator.remove(), 300);
}, 50);
}
// Release a note
function releaseNote(note) {
let octave = 4;
if (note.endsWith('1')) {
note = note.replace('1', '');
octave = 5;
}
synth.triggerRelease();
}
// Patch cable interaction
cablePorts.forEach(port => {
port.addEventListener('mousedown', (e) => {
draggingCable = true;
cableStart = {
x: e.clientX,
y: e.clientY,
element: port
};
port.classList.add('active');
e.stopPropagation();
});
});
document.addEventListener('mousemove', (e) => {
if (!draggingCable) return;
const startRect = cableStart.element.getBoundingClientRect();
const startX = startRect.left + startRect.width / 2;
const startY = startRect.top + startRect.height / 2;
// Update cable path
patchCable.setAttribute('d', `M${startX},${startY} L${e.clientX},${e.clientY}`);
});
document.addEventListener('mouseup', (e) => {
if (!draggingCable) return;
// Find if we're dropping on another port
const endElement = document.elementFromPoint(e.clientX, e.clientY);
const endPort = endElement?.closest('.cable-port');
if (endPort && endPort !== cableStart.element) {
// Connect between these ports (in a real app, we'd implement the actual connection)
const startRect = cableStart.element.getBoundingClientRect();
const endRect = endPort.getBoundingClientRect();
const startX = startRect.left + startRect.width / 2;
const startY = startRect.top + startRect.height / 2;
const endX = endRect.left + endRect.width / 2;
const endY = endRect.top + endRect.height / 2;
patchCable.setAttribute('d', `M${startX},${startY} L${endX},${endY}`);
// Visual feedback for connected ports
cableStart.element.classList.add('active');
endPort.classList.add('active');
} else {
// Reset cable if not dropped on another port
patchCable.setAttribute('d', '');
cableStart.element.classList.remove('active');
}
draggingCable = false;
cableStart = null;
});
// Oscilloscope visualization
const waveform = document.querySelector('.waveform');
const lfoIndicator = document.querySelector('.lfo-indicator');
function updateVisualization() {
const now = Tone.now();
const lfoValue = lfo.getValueAtTime(now);
const lfoPos = (lfoValue + 1) / 2 * 100; // Convert to 0-100%
lfoIndicator.style.left = `${lfoPos}%`;
// Very simple waveform visualization (in a real app, use Tone.Visualizer)
const sinValue = Math.sin(now * 10) * 40;
waveform.style.transform = `translateY(${sinValue}px)`;
requestAnimationFrame(updateVisualization);
}
// Start with sine wave selected
document.querySelector('[data-wave="sine"]').click();
document.querySelector('[data-lfo="sine"]').click();
// Start visualization
updateVisualization();
// Set initial knob rotations based on default values
Object.keys(knobs).forEach(key => {
const knob = knobs[key];
const normalized = (knob.value - knob.min) / (knob.max - knob.min);
const rotation = (normalized * 300) - 150; // -150 to 150 degrees
updateKnob(knob.element, rotation);
});
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=fluxthedev/modular-66" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>