analog-synth / index.html
Tingchenliang's picture
Make the keyboard usable: when I click the notes, it'd make correct pitch - Initial Deployment
fbba045 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vintage Analog Synth</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
synth: {
dark: '#1a1a2e',
panel: '#2d2d44',
knob: '#4a4a6a',
highlight: '#ff6b6b',
wood: '#8b4513',
metal: '#a9a9a9',
label: '#d1d1e9'
}
},
fontFamily: {
'orbitron': ['Orbitron', 'sans-serif'],
'audiowide': ['Audiowide', 'cursive']
}
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Audiowide&display=swap" rel="stylesheet">
<style>
@keyframes glow {
0% { box-shadow: 0 0 5px #ff6b6b; }
50% { box-shadow: 0 0 20px #ff6b6b; }
100% { box-shadow: 0 0 5px #ff6b6b; }
}
.knob {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #4a4a6a 0%, #2d2d44 100%);
box-shadow:
inset 0 0 10px rgba(0, 0, 0, 0.5),
0 5px 15px rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: transform 0.1s;
}
.knob::before {
content: '';
position: absolute;
top: 5px;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 20px;
background: #ff6b6b;
border-radius: 3px;
}
.knob-label {
position: absolute;
bottom: -25px;
left: 0;
width: 100%;
text-align: center;
font-size: 0.75rem;
color: #d1d1e9;
}
.wood-panel {
background: linear-gradient(
to bottom,
#8b4513 0%,
#5d2e0a 20%,
#8b4513 40%,
#5d2e0a 60%,
#8b4513 80%,
#5d2e0a 100%
);
border: 2px solid #5d2e0a;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
.metal-frame {
background: linear-gradient(135deg, #c0c0c0 0%, #a9a9a9 100%);
border: 1px solid #888;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
.key-white {
width: 40px;
height: 180px;
background: linear-gradient(to bottom, #fff 0%, #f0f0f0 100%);
border: 1px solid #ccc;
border-radius: 0 0 5px 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 1;
}
.key-black {
width: 28px;
height: 120px;
background: linear-gradient(to bottom, #000 0%, #333 100%);
border: 1px solid #000;
border-radius: 0 0 4px 4px;
position: absolute;
z-index: 2;
margin-left: -14px;
}
.key-white.active {
background: linear-gradient(to bottom, #ffd700 0%, #ffb700 100%);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.3);
}
.key-black.active {
background: linear-gradient(to bottom, #ff8c00 0%, #ff6b00 100%);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
.led {
width: 12px;
height: 12px;
border-radius: 50%;
background: #444;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
}
.led.active {
background: #ff6b6b;
box-shadow: 0 0 10px #ff6b6b;
animation: glow 1.5s infinite;
}
.vintage-label {
font-family: 'Audiowide', cursive;
color: #d1d1e9;
text-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
letter-spacing: 1px;
}
.panel-label {
font-family: 'Audiowide', cursive;
color: #ff6b6b;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
.synth-container {
perspective: 1000px;
}
.synth-body {
transform: rotateX(5deg);
transform-style: preserve-3d;
}
.knob-container {
transform-style: preserve-3d;
}
.knob-shadow {
position: absolute;
bottom: -5px;
left: 5px;
width: 50px;
height: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
filter: blur(5px);
z-index: -1;
}
.lcd-display {
background: linear-gradient(135deg, #0a3f0a 0%, #082908 100%);
border: 2px solid #0f5c0f;
color: #7fff7f;
font-family: 'Audiowide', cursive;
text-shadow: 0 0 5px #7fff7f;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
.lcd-text {
animation: flicker 0.1s infinite alternate;
}
@keyframes flicker {
0% { opacity: 0.9; }
100% { opacity: 1; }
}
</style>
</head>
<body class="bg-synth-dark min-h-screen flex flex-col items-center justify-center p-4 font-orbitron">
<div class="synth-container w-full max-w-4xl">
<div class="synth-body bg-synth-panel rounded-xl p-6 shadow-2xl">
<!-- Header -->
<div class="text-center mb-6">
<h1 class="vintage-label text-3xl md:text-4xl mb-2">ANALOG SYNTHESIZER</h1>
<div class="lcd-display rounded-lg p-3 mx-auto max-w-md">
<div class="lcd-text text-center text-lg">MODEL: V-1978</div>
</div>
</div>
<!-- Main Controls -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- Oscillator Section -->
<div class="bg-synth-dark rounded-lg p-4 wood-panel">
<div class="panel-label text-center mb-3">OSCILLATOR</div>
<div class="grid grid-cols-3 gap-4">
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="osc1-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">WAVE</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="osc2-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">OCTAVE</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="detune-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">DETUNE</div>
</div>
</div>
<div class="flex justify-around mt-4">
<div class="flex flex-col items-center">
<div class="led" id="sine-led"></div>
<span class="text-xs mt-1 text-synth-label">SINE</span>
</div>
<div class="flex flex-col items-center">
<div class="led active" id="saw-led"></div>
<span class="text-xs mt-1 text-synth-label">SAW</span>
</div>
<div class="flex flex-col items-center">
<div class="led" id="square-led"></div>
<span class="text-xs mt-1 text-synth-label">SQUARE</span>
</div>
<div class="flex flex-col items-center">
<div class="led" id="triangle-led"></div>
<span class="text-xs mt-1 text-synth-label">TRI</span>
</div>
</div>
</div>
<!-- Filter Section -->
<div class="bg-synth-dark rounded-lg p-4 wood-panel">
<div class="panel-label text-center mb-3">FILTER</div>
<div class="grid grid-cols-3 gap-4">
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="cutoff-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">CUTOFF</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="resonance-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">RESONANCE</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="env-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">ENV AMT</div>
</div>
</div>
<div class="flex justify-around mt-4">
<div class="flex flex-col items-center">
<div class="led active" id="lowpass-led"></div>
<span class="text-xs mt-1 text-synth-label">LOW</span>
</div>
<div class="flex flex-col items-center">
<div class="led" id="highpass-led"></div>
<span class="text-xs mt-1 text-synth-label">HIGH</span>
</div>
<div class="flex flex-col items-center">
<div class="led" id="bandpass-led"></div>
<span class="text-xs mt-1 text-synth-label">BAND</span>
</div>
</div>
</div>
<!-- Envelope Section -->
<div class="bg-synth-dark rounded-lg p-4 wood-panel">
<div class="panel-label text-center mb-3">ENVELOPE</div>
<div class="grid grid-cols-4 gap-3">
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="attack-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">ATTACK</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="decay-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">DECAY</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="sustain-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">SUSTAIN</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="release-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">RELEASE</div>
</div>
</div>
<div class="mt-4 flex justify-center">
<div class="bg-synth-panel rounded p-2 w-full">
<div class="h-2 bg-gray-800 rounded-full overflow-hidden">
<div class="h-full bg-synth-highlight rounded-full w-3/4"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Modulation and Effects -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-synth-dark rounded-lg p-4 wood-panel">
<div class="panel-label text-center mb-3">MODULATION</div>
<div class="grid grid-cols-3 gap-4">
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="lfo-rate-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">RATE</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="lfo-amount-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">AMOUNT</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="lfo-target-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">TARGET</div>
</div>
</div>
</div>
<div class="bg-synth-dark rounded-lg p-4 wood-panel">
<div class="panel-label text-center mb-3">EFFECTS</div>
<div class="grid grid-cols-3 gap-4">
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="delay-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">DELAY</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="reverb-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">REVERB</div>
</div>
<div class="knob-container flex flex-col items-center">
<div class="knob relative" id="distortion-knob">
<div class="knob-shadow"></div>
</div>
<div class="knob-label">DRIVE</div>
</div>
</div>
</div>
</div>
<!-- Keyboard -->
<div class="bg-synth-dark rounded-lg p-4 wood-panel mb-6">
<div class="panel-label text-center mb-3">KEYBOARD</div>
<div class="relative h-48 flex justify-center">
<!-- White Keys -->
<div class="flex relative">
<div class="key-white" data-note="C" data-frequency="261.63"></div>
<div class="key-white" data-note="D" data-frequency="293.66"></div>
<div class="key-white" data-note="E" data-frequency="329.63"></div>
<div class="key-white" data-note="F" data-frequency="349.23"></div>
<div class="key-white" data-note="G" data-frequency="392.00"></div>
<div class="key-white" data-note="A" data-frequency="440.00"></div>
<div class="key-white" data-note="B" data-frequency="493.88"></div>
<div class="key-white" data-note="C2" data-frequency="523.25"></div>
<!-- Black Keys -->
<div class="key-black" style="left: 30px;" data-note="C#" data-frequency="277.18"></div>
<div class="key-black" style="left: 70px;" data-note="D#" data-frequency="311.13"></div>
<div class="key-black" style="left: 150px;" data-note="F#" data-frequency="369.99"></div>
<div class="key-black" style="left: 190px;" data-note="G#" data-frequency="415.30"></div>
<div class="key-black" style="left: 230px;" data-note="A#" data-frequency="466.16"></div>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex justify-between items-center">
<div class="flex items-center">
<div class="led active mr-2"></div>
<span class="text-sm text-synth-label">POWER ON</span>
</div>
<div class="text-sm text-synth-label">
<i class="fas fa-copyright mr-1"></i> 1978 ANALOG SYSTEMS
</div>
<div class="flex">
<button class="metal-frame px-3 py-1 rounded text-synth-label mr-2 hover:bg-synth-highlight hover:text-white transition">
PRESETS
</button>
<button class="metal-frame px-3 py-1 rounded text-synth-label hover:bg-synth-highlight hover:text-white transition">
RECORD
</button>
</div>
</div>
</div>
</div>
<script>
// Initialize Web Audio API
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
let oscillator = null;
let gainNode = null;
let filter = null;
// Initialize synth parameters
const synthParams = {
waveform: 'sawtooth',
octave: 0,
detune: 0,
cutoff: 1000,
resonance: 1,
attack: 0.1,
decay: 0.3,
sustain: 0.7,
release: 0.5
};
// Note frequencies
const noteFrequencies = {
'C': 261.63,
'C#': 277.18,
'D': 293.66,
'D#': 311.13,
'E': 329.63,
'F': 349.23,
'F#': 369.99,
'G': 392.00,
'G#': 415.30,
'A': 440.00,
'A#': 466.16,
'B': 493.88,
'C2': 523.25
};
// Initialize knobs
function initKnobs() {
const knobs = document.querySelectorAll('.knob');
knobs.forEach(knob => {
let isDragging = false;
let startY = 0;
let startRotation = 0;
let currentRotation = 0;
knob.addEventListener('mousedown', (e) => {
isDragging = true;
startY = e.clientY;
startRotation = currentRotation;
knob.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaY = startY - e.clientY;
currentRotation = Math.min(135, Math.max(-135, startRotation + deltaY));
knob.style.transform = `rotate(${currentRotation}deg)`;
// Simulate sound when adjusting knobs
playKnobSound();
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
knob.style.cursor = 'pointer';
}
});
knob.addEventListener('touchstart', (e) => {
isDragging = true;
startY = e.touches[0].clientY;
startRotation = currentRotation;
});
document.addEventListener('touchmove', (e) => {
if (isDragging) {
const deltaY = startY - e.touches[0].clientY;
currentRotation = Math.min(135, Math.max(-135, startRotation + deltaY));
knob.style.transform = `rotate(${currentRotation}deg)`;
playKnobSound();
}
});
document.addEventListener('touchend', () => {
isDragging = false;
});
});
}
// Play a sound when turning knobs
function playKnobSound() {
if (oscillator) {
oscillator.stop();
}
oscillator = audioContext.createOscillator();
gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.value = 200;
gainNode.gain.value = 0.1;
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.1);
}
// Play a note
function playNote(noteElement) {
if (oscillator) {
oscillator.stop();
}
oscillator = audioContext.createOscillator();
gainNode = audioContext.createGain();
filter = audioContext.createBiquadFilter();
// Get frequency from data attribute
const frequency = parseFloat(noteElement.dataset.frequency) * Math.pow(2, synthParams.octave);
// Set oscillator parameters
oscillator.type = synthParams.waveform;
oscillator.frequency.value = frequency;
oscillator.detune.value = synthParams.detune;
// Set filter parameters
filter.type = 'lowpass';
filter.frequency.value = synthParams.cutoff;
filter.Q.value = synthParams.resonance;
// Set envelope parameters
const now = audioContext.currentTime;
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(1, now + synthParams.attack);
gainNode.gain.linearRampToValueAtTime(synthParams.sustain, now + synthParams.attack + synthParams.decay);
// Connect nodes
oscillator.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
}
// Release note
function releaseNote() {
const now = audioContext.currentTime;
gainNode.gain.cancelScheduledValues(now);
gainNode.gain.setValueAtTime(gainNode.gain.value, now);
gainNode.gain.linearRampToValueAtTime(0, now + synthParams.release);
setTimeout(() => {
if (oscillator) {
oscillator.stop();
}
}, synthParams.release * 1000);
}
// Initialize keyboard
function initKeyboard() {
const keys = document.querySelectorAll('.key-white, .key-black');
keys.forEach(key => {
key.addEventListener('mousedown', () => {
key.classList.add('active');
playNote(key);
});
key.addEventListener('mouseup', () => {
key.classList.remove('active');
releaseNote();
});
key.addEventListener('mouseleave', () => {
if (key.classList.contains('active')) {
key.classList.remove('active');
releaseNote();
}
});
// Touch events for mobile
key.addEventListener('touchstart', (e) => {
e.preventDefault();
key.classList.add('active');
playNote(key);
});
key.addEventListener('touchend', () => {
key.classList.remove('active');
releaseNote();
});
});
}
// Initialize waveform LEDs
function initWaveformLEDs() {
const leds = document.querySelectorAll('#sine-led, #saw-led, #square-led, #triangle-led');
leds.forEach(led => {
led.addEventListener('click', () => {
// Remove active class from all
leds.forEach(l => l.classList.remove('active'));
// Add active to clicked
led.classList.add('active');
// Update waveform
if (led.id === 'sine-led') synthParams.waveform = 'sine';
if (led.id === 'saw-led') synthParams.waveform = 'sawtooth';
if (led.id === 'square-led') synthParams.waveform = 'square';
if (led.id === 'triangle-led') synthParams.waveform = 'triangle';
playKnobSound();
});
});
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
initKnobs();
initKeyboard();
initWaveformLEDs();
// Add slight rotation to knobs for visual effect
const knobs = document.querySelectorAll('.knob');
knobs.forEach((knob, i) => {
const rotation = -90 + (i * 20);
knob.style.transform = `rotate(${rotation}deg)`;
});
});
</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=Tingchenliang/analog-synth" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>