Skip to content

Instantly share code, notes, and snippets.

@lerouxb
Created November 28, 2025 19:46
Show Gist options
  • Select an option

  • Save lerouxb/6867db7c122d17aa6af922d39fa83ac6 to your computer and use it in GitHub Desktop.

Select an option

Save lerouxb/6867db7c122d17aa6af922d39fa83ac6 to your computer and use it in GitHub Desktop.
Trying out cursor: Creating an FM synthesizer

Build a simple FM synthesizer

Exported on 27/11/2025 at 8:38:32 GMT from Cursor (2.1.36)


User

can you write an audio synthesizer using web audio. It does simple two-operator FM synthesis. It has a knob for the carrier frequency, a knob for the modulator frequency and a knob for the modulation depth.


Cursor

I'll create a beautiful FM synthesizer with Web Audio API. Let me build this with a distinctive, retro-futuristic aesthetic.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>

I've created a beautiful two-operator FM synthesizer with a retro-futuristic aesthetic. Here's what it includes:

Features:

  • Carrier Frequency Knob (cyan) - Controls the main oscillator frequency (20-2000 Hz)
  • Modulator Frequency Knob (magenta) - Controls the modulating oscillator frequency (1-2000 Hz)
  • Modulation Depth Knob (orange) - Controls how much the modulator affects the carrier (0-1000)
  • Real-time waveform visualizer showing the FM output
  • Animated LED strip that pulses when playing
  • Play/Stop button to toggle audio

Design:

  • Dark panel with glowing neon accents (cyan, magenta, orange)
  • Animated grid background with floating gradient orbs
  • Orbitron and Share Tech Mono fonts for a sci-fi feel
  • Interactive knobs with glow effects (drag up/down to adjust)
  • Smooth parameter transitions to avoid clicks

How FM Synthesis works here: The modulator oscillator's output is connected to the carrier oscillator's frequency input through a gain node. This means the carrier frequency is constantly being modulated at the modulator's rate, creating rich harmonic content. Higher modulation depth = more dramatic timbral changes.

To try it out, just open the HTML file in a browser:

The synth is now open in Safari!

To use it:

  1. Click the ▶ PLAY button to start the audio
  2. Drag the knobs up/down to adjust parameters
  3. Try different combinations - low carrier + high modulator creates bell-like tones, while matching frequencies create rich harmonic content
  4. The depth knob dramatically changes the timbre - low values are subtle, high values create aggressive, metallic sounds

Enjoy experimenting with FM synthesis! 🎛️


User

It works great! can we add a simple decay-only envelope control and an LFO knob that will be used to trigger the envelope?


Cursor

I'll add a decay envelope and an LFO that triggers it rhythmically. This will create pulsing, rhythmic tones!

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="controls">
      <div class="knob-container">
        <div class="knob-label cyan">CARRIER</div>
        <div class="knob-wrapper">
          <div class="knob-ring cyan"></div>
          <div class="knob" id="carrierKnob">
            <div class="knob-indicator cyan"></div>
          </div>
        </div>
        <div class="knob-value" id="carrierValue">220 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label magenta">MODULATOR</div>
        <div class="knob-wrapper">
          <div class="knob-ring magenta"></div>
          <div class="knob" id="modulatorKnob">
            <div class="knob-indicator magenta"></div>
          </div>
        </div>
        <div class="knob-value" id="modulatorValue">440 Hz</div>
      </div>

      <div class="knob-container">
        <div class="knob-label orange">DEPTH</div>
        <div class="knob-wrapper">
          <div class="knob-ring orange"></div>
          <div class="knob" id="depthKnob">
            <div class="knob-indicator orange"></div>
          </div>
        </div>
        <div class="knob-value" id="depthValue">100</div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        drawWaveform();
        animateLEDs();
      } else {
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 2000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>

User

great! can the modulator go to 10khz?


Cursor

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1000,
        value: 100,
        param: 'modulationDepth',
        display: (v) => Math.round(v),
        valueEl: 'depthValue'
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>

Done! The modulator now goes up to 10 kHz. I also updated the display to show "kHz" when the frequency is 1000 Hz or higher (e.g., "2.5 kHz" instead of "2500 Hz") for better readability.


User

Awesome. Can the depth modulate the phase from 0 to 1? Where 1 advances the since wave one full cycle.


Cursor

I'll update the modulation depth to work as a phase modulation index, where 1 = one full cycle (2π radians) of phase deviation.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue'
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      modulationGain.gain.value = params.modulationDepth;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
      modulationGain.gain.setTargetAtTime(params.modulationDepth, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 100,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>

Done! The depth knob now goes from 0 to 1, where:

  • 0 = no modulation
  • 1 = one full cycle (2π radians) of phase modulation

The actual frequency deviation is calculated as depth × 2π × modulatorFreq, which gives you proper phase modulation where the index is normalized to cycles. The default is now 0.20 for a nice starting point.


User

awesome. Let's add a 4 step sequencer. The carrier frequency knob is now used as the base frequency. Each time the LFO triggers it advances a step. Steps loop from step 1 to 2 to 3 to 4 back to 1. For each step there is a knob that sets a pitch offset. The most recent step's pitch offset is added on to the base frequency to form the carrier frequency.


Cursor

I'll add a 4-step sequencer with pitch offset knobs. Each step will have a semitone offset that gets added to the base frequency.

I'll add the 4-step sequencer with pitch offset knobs. Let me make several changes:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">CARRIER</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="carrierKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="carrierValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;

    // Synth Parameters
    const params = {
      carrierFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      carrierKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'carrierFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'carrierValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      carrierKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values
      carrierOsc.frequency.value = params.carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      carrierOsc.frequency.setTargetAtTime(params.carrierFreq, now, 0.01);
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        params[config.param] = newValue;
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('carrierKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain()
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
        currentStep = 0; // Reset sequencer
        updateStepLEDs();
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>

Done! I've added a 4-step sequencer to the synth. Here's what's new:

Changes:

  • BASE FREQ knob (was CARRIER) - Sets the base frequency for the sequencer
  • SEQUENCER section with 4 step knobs:
    • Each step has a semitone offset (-24 to +24, i.e., ±2 octaves)
    • Step indicators light up violet to show the current step
    • Steps advance 1→2→3→4→1... each time the LFO triggers

How it works:

  • The actual carrier frequency = baseFreq × 2^(semitones/12)
  • Set different semitone offsets on each step to create melodic patterns
  • For example: Step 1 = 0, Step 2 = +5, Step 3 = +7, Step 4 = +12 creates a nice arpeggio

Display:

  • Shows semitones with st suffix (e.g., "+7 st", "-5 st", "0 st")
  • The step LED indicators pulse in sync with the LFO to show which step is playing

You can refresh the browser to see the updated synth!


User

can the base frequency and modulator frequency knobs follow an exponential curve rather than linear?


Cursor

I'll add exponential curve support to the knob system for the frequency knobs.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue',
        exponential: true
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain(),
        exponential: true
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
        currentStep = 0; // Reset sequencer
        updateStepLEDs();
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value
      const valueToAngle = (value) => {
        const normalized = (value - config.min) / (config.max - config.min);
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        return config.min + normalized * (config.max - config.min);
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${Math.round(v)} Hz`,
        valueEl: 'baseFreqValue',
        exponential: true
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain(),
        exponential: true
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
        currentStep = 0; // Reset sequencer
        updateStepLEDs();
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value (supports exponential scaling)
      const valueToAngle = (value) => {
        let normalized;
        if (config.exponential) {
          // Exponential: normalized = log(value/min) / log(max/min)
          normalized = Math.log(value / config.min) / Math.log(config.max / config.min);
        } else {
          normalized = (value - config.min) / (config.max - config.min);
        }
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        if (config.exponential) {
          // Exponential: value = min * (max/min)^normalized
          return config.min * Math.pow(config.max / config.min, normalized);
        } else {
          return config.min + normalized * (config.max - config.min);
        }
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>

Done! Both the BASE FREQ and MODULATOR knobs now follow an exponential curve.

How it works:

  • Linear: Equal knob rotation = equal Hz change (e.g., every 10° adds 100 Hz)
  • Exponential: Equal knob rotation = equal ratio change (e.g., every 10° doubles the frequency)

This feels much more natural for pitch because human hearing is logarithmic. Now:

  • The lower range of frequencies has more precision (great for bass tones)
  • The middle of the knob range gives you the geometric mean of min/max
  • Higher frequencies are still accessible but don't dominate the knob range

For example, with the modulator (1 Hz to 10 kHz):

  • Knob at 25% ≈ 10 Hz
  • Knob at 50% ≈ 100 Hz
  • Knob at 75% ≈ 1 kHz
  • Knob at 100% = 10 kHz

Refresh to try it out!


User

can the base frequency knob be quantized to the chromatic scale?


Cursor

I'll add chromatic scale quantization to the base frequency knob, and show the note name in the display.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${frequencyToNoteName(v)}`,
        valueEl: 'baseFreqValue',
        exponential: true,
        quantize: 'chromatic'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain(),
        exponential: true
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
        currentStep = 0; // Reset sequencer
        updateStepLEDs();
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value (supports exponential scaling)
      const valueToAngle = (value) => {
        let normalized;
        if (config.exponential) {
          // Exponential: normalized = log(value/min) / log(max/min)
          normalized = Math.log(value / config.min) / Math.log(config.max / config.min);
        } else {
          normalized = (value - config.min) / (config.max - config.min);
        }
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        if (config.exponential) {
          // Exponential: value = min * (max/min)^normalized
          return config.min * Math.pow(config.max / config.min, normalized);
        } else {
          return config.min + normalized * (config.max - config.min);
        }
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
    }

    .knob-ring.cyan { border-color: rgba(0, 240, 255, 0.3); }
    .knob-ring.magenta { border-color: rgba(255, 0, 170, 0.3); }
    .knob-ring.orange { border-color: rgba(255, 107, 0, 0.3); }
    .knob-ring.green { border-color: rgba(0, 255, 136, 0.3); }
    .knob-ring.yellow { border-color: rgba(255, 238, 0, 0.3); }
    .knob-ring.violet { border-color: rgba(170, 102, 255, 0.3); }

    .knob-value {
      font-family: 'Orbitron', sans-serif;
      font-size: 1rem;
      font-weight: 700;
      color: var(--text-bright);
      min-width: 80px;
      text-align: center;
      padding: 6px 12px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .power-section {
      margin-top: 48px;
      display: flex;
      justify-content: center;
    }

    .power-button {
      font-family: 'Orbitron', sans-serif;
      font-weight: 700;
      font-size: 0.9rem;
      letter-spacing: 0.3em;
      padding: 16px 48px;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      border-radius: 50px;
      color: var(--text-dim);
      cursor: pointer;
      transition: all 0.3s ease;
      position: relative;
      overflow: hidden;
    }

    .power-button::before {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    .power-button span {
      position: relative;
      z-index: 1;
    }

    .power-button:hover {
      border-color: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    .power-button.active {
      border-color: var(--accent-magenta);
      color: var(--text-bright);
      box-shadow: var(--glow-magenta);
    }

    .power-button.active::before {
      opacity: 0.2;
    }

    /* Waveform visualizer */
    .visualizer {
      margin-top: 32px;
      height: 80px;
      background: rgba(0, 0, 0, 0.4);
      border-radius: 12px;
      border: 1px solid rgba(255, 255, 255, 0.1);
      overflow: hidden;
      position: relative;
    }

    .visualizer canvas {
      width: 100%;
      height: 100%;
    }

    /* LED indicators */
    .led-strip {
      display: flex;
      justify-content: center;
      gap: 4px;
      margin-top: 24px;
    }

    .led {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .led.active {
      background: var(--accent-cyan);
      box-shadow: var(--glow-cyan);
    }

    /* Instructions */
    .instructions {
      text-align: center;
      margin-top: 24px;
      font-size: 0.7rem;
      color: var(--text-dim);
      letter-spacing: 0.1em;
    }

    /* Sequencer styles */
    .sequencer-controls {
      display: flex;
      gap: 24px;
      justify-content: center;
    }

    .step-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }

    .step-indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: var(--text-dim);
      transition: all 0.1s ease;
    }

    .step-indicator.active {
      background: var(--accent-violet);
      box-shadow: var(--glow-violet);
    }

    .step-container .knob-wrapper {
      width: 80px;
      height: 80px;
    }

    .step-container .knob-indicator {
      height: 12px;
    }

    .step-container .knob-value {
      font-size: 0.8rem;
      min-width: 60px;
      padding: 4px 8px;
    }

    .step-container .knob-label {
      font-size: 0.6rem;
    }
  </style>
</head>
<body>
  <div class="orb orb-1"></div>
  <div class="orb orb-2"></div>

  <div class="synth-panel">
    <header>
      <h1>FM SYNTH</h1>
      <div class="subtitle">TWO OPERATOR FREQUENCY MODULATION</div>
    </header>

    <div class="control-section">
      <div class="section-label">FM OSCILLATORS</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label cyan">BASE FREQ</div>
          <div class="knob-wrapper">
            <div class="knob-ring cyan"></div>
            <div class="knob" id="baseFreqKnob">
              <div class="knob-indicator cyan"></div>
            </div>
          </div>
          <div class="knob-value" id="baseFreqValue">220 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label magenta">MODULATOR</div>
          <div class="knob-wrapper">
            <div class="knob-ring magenta"></div>
            <div class="knob" id="modulatorKnob">
              <div class="knob-indicator magenta"></div>
            </div>
          </div>
          <div class="knob-value" id="modulatorValue">440 Hz</div>
        </div>

        <div class="knob-container">
          <div class="knob-label orange">DEPTH</div>
          <div class="knob-wrapper">
            <div class="knob-ring orange"></div>
            <div class="knob" id="depthKnob">
              <div class="knob-indicator orange"></div>
            </div>
          </div>
          <div class="knob-value" id="depthValue">100</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">SEQUENCER</div>
      <div class="sequencer-controls">
        <div class="step-container">
          <div class="step-indicator" id="stepLed0"></div>
          <div class="knob-label violet">STEP 1</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step0Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step0Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed1"></div>
          <div class="knob-label violet">STEP 2</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step1Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step1Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed2"></div>
          <div class="knob-label violet">STEP 3</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step2Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step2Value">0 st</div>
        </div>

        <div class="step-container">
          <div class="step-indicator" id="stepLed3"></div>
          <div class="knob-label violet">STEP 4</div>
          <div class="knob-wrapper">
            <div class="knob-ring violet"></div>
            <div class="knob" id="step3Knob">
              <div class="knob-indicator violet"></div>
            </div>
          </div>
          <div class="knob-value" id="step3Value">0 st</div>
        </div>
      </div>
    </div>

    <div class="control-section">
      <div class="section-label">ENVELOPE &amp; LFO</div>
      <div class="controls">
        <div class="knob-container">
          <div class="knob-label green">DECAY</div>
          <div class="knob-wrapper">
            <div class="knob-ring green"></div>
            <div class="knob" id="decayKnob">
              <div class="knob-indicator green"></div>
            </div>
          </div>
          <div class="knob-value" id="decayValue">200 ms</div>
        </div>

        <div class="knob-container">
          <div class="knob-label yellow">LFO RATE</div>
          <div class="knob-wrapper">
            <div class="knob-ring yellow"></div>
            <div class="knob" id="lfoKnob">
              <div class="knob-indicator yellow"></div>
            </div>
          </div>
          <div class="knob-value" id="lfoValue">4.0 Hz</div>
        </div>
      </div>
    </div>

    <div class="visualizer">
      <canvas id="waveform"></canvas>
    </div>

    <div class="led-strip">
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
      <div class="led"></div>
    </div>

    <div class="power-section">
      <button class="power-button" id="powerBtn">
        <span>▶ PLAY</span>
      </button>
    </div>

    <div class="instructions">
      DRAG KNOBS TO ADJUST • CLICK PLAY TO START
    </div>
  </div>

  <script>
    // Audio Context and Nodes
    let audioContext = null;
    let carrierOsc = null;
    let modulatorOsc = null;
    let modulationGain = null;
    let envelopeGain = null;
    let masterGain = null;
    let analyser = null;
    let isPlaying = false;
    let lfoIntervalId = null;
    let currentStep = 0;

    // Synth Parameters
    const params = {
      baseFreq: 220,
      modulatorFreq: 440,
      modulationDepth: 0.2,
      decay: 0.2,
      lfoRate: 4,
      stepOffsets: [0, 0, 0, 0]  // Semitone offsets for each step
    };

    // Knob configuration
    const knobConfig = {
      baseFreqKnob: {
        min: 20,
        max: 2000,
        value: 220,
        param: 'baseFreq',
        display: (v) => `${frequencyToNoteName(v)}`,
        valueEl: 'baseFreqValue',
        exponential: true,
        quantize: 'chromatic'
      },
      modulatorKnob: {
        min: 1,
        max: 10000,
        value: 440,
        param: 'modulatorFreq',
        display: (v) => v >= 1000 ? `${(v/1000).toFixed(1)} kHz` : `${Math.round(v)} Hz`,
        valueEl: 'modulatorValue',
        onChange: () => updateModulationGain(),
        exponential: true
      },
      depthKnob: {
        min: 0,
        max: 1,
        value: 0.2,
        param: 'modulationDepth',
        display: (v) => v.toFixed(2),
        valueEl: 'depthValue',
        onChange: () => updateModulationGain()
      },
      decayKnob: {
        min: 0.01,
        max: 2,
        value: 0.2,
        param: 'decay',
        display: (v) => `${Math.round(v * 1000)} ms`,
        valueEl: 'decayValue'
      },
      lfoKnob: {
        min: 0.1,
        max: 20,
        value: 4,
        param: 'lfoRate',
        display: (v) => `${v.toFixed(1)} Hz`,
        valueEl: 'lfoValue',
        onChange: () => updateLFO()
      },
      step0Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 0,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step0Value'
      },
      step1Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 1,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step1Value'
      },
      step2Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 2,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step2Value'
      },
      step3Knob: {
        min: -24,
        max: 24,
        value: 0,
        stepIndex: 3,
        display: (v) => `${v > 0 ? '+' : ''}${Math.round(v)} st`,
        valueEl: 'step3Value'
      }
    };

    // Note names for chromatic scale
    const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
    
    // Convert frequency to nearest chromatic note (A4 = 440Hz)
    function quantizeToChromatic(freq) {
      const semitones = 12 * Math.log2(freq / 440);
      const roundedSemitones = Math.round(semitones);
      return 440 * Math.pow(2, roundedSemitones / 12);
    }
    
    // Convert frequency to note name with octave (e.g., "A4", "C#5")
    function frequencyToNoteName(freq) {
      const semitones = Math.round(12 * Math.log2(freq / 440));
      const noteIndex = ((semitones % 12) + 12 + 9) % 12; // A is at index 9 in noteNames (0=C)
      const octave = Math.floor((semitones + 9) / 12) + 4; // A4 is octave 4
      return `${noteNames[noteIndex]}${octave}`;
    }

    // Initialize knob angles (in degrees, 0 = top, range: -135 to 135)
    const knobAngles = {
      baseFreqKnob: 0,
      modulatorKnob: 45,
      depthKnob: -90,
      decayKnob: 0,
      lfoKnob: 0,
      step0Knob: 0,
      step1Knob: 0,
      step2Knob: 0,
      step3Knob: 0
    };

    // Initialize audio
    function initAudio() {
      audioContext = new (window.AudioContext || window.webkitAudioContext)();
      
      // Create oscillators
      carrierOsc = audioContext.createOscillator();
      modulatorOsc = audioContext.createOscillator();
      
      carrierOsc.type = 'sine';
      modulatorOsc.type = 'sine';
      
      // Create gain nodes
      modulationGain = audioContext.createGain();
      envelopeGain = audioContext.createGain();
      masterGain = audioContext.createGain();
      
      // Create analyser for visualization
      analyser = audioContext.createAnalyser();
      analyser.fftSize = 2048;
      
      // Set initial values - calculate carrier from base freq + current step offset
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.value = carrierFreq;
      modulatorOsc.frequency.value = params.modulatorFreq;
      // Modulation gain: depth * 2π * modulatorFreq (where depth=1 means 1 full cycle)
      modulationGain.gain.value = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      envelopeGain.gain.value = 0;
      masterGain.gain.value = 0.3;
      
      // Connect the FM synthesis chain
      // Modulator -> ModulationGain -> Carrier.frequency
      modulatorOsc.connect(modulationGain);
      modulationGain.connect(carrierOsc.frequency);
      
      // Carrier -> EnvelopeGain -> MasterGain -> Analyser -> Destination
      carrierOsc.connect(envelopeGain);
      envelopeGain.connect(masterGain);
      masterGain.connect(analyser);
      analyser.connect(audioContext.destination);
      
      // Start oscillators
      carrierOsc.start();
      modulatorOsc.start();
    }

    // Calculate carrier frequency from base frequency + current step's semitone offset
    function getCarrierFrequency() {
      const semitones = params.stepOffsets[currentStep];
      return params.baseFreq * Math.pow(2, semitones / 12);
    }

    // Update step LED indicators
    function updateStepLEDs() {
      for (let i = 0; i < 4; i++) {
        const led = document.getElementById(`stepLed${i}`);
        if (led) {
          led.classList.toggle('active', i === currentStep);
        }
      }
    }

    // Trigger the envelope (called by LFO)
    function triggerEnvelope() {
      if (!audioContext || !envelopeGain) return;
      
      const now = audioContext.currentTime;
      
      // Update carrier frequency for current step
      const carrierFreq = getCarrierFrequency();
      carrierOsc.frequency.setValueAtTime(carrierFreq, now);
      
      // Update step LEDs
      updateStepLEDs();
      
      // Cancel any scheduled changes
      envelopeGain.gain.cancelScheduledValues(now);
      
      // Start at full volume immediately
      envelopeGain.gain.setValueAtTime(1, now);
      
      // Decay to zero over the decay time
      envelopeGain.gain.exponentialRampToValueAtTime(0.001, now + params.decay);
      
      // Advance to next step for next trigger
      currentStep = (currentStep + 1) % 4;
    }

    // Start/update the LFO timer
    function updateLFO() {
      if (!isPlaying) return;
      
      // Clear existing interval
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
      }
      
      // Calculate interval in milliseconds
      const intervalMs = 1000 / params.lfoRate;
      
      // Start new interval
      lfoIntervalId = setInterval(triggerEnvelope, intervalMs);
      
      // Trigger immediately
      triggerEnvelope();
    }

    // Stop the LFO
    function stopLFO() {
      if (lfoIntervalId) {
        clearInterval(lfoIntervalId);
        lfoIntervalId = null;
      }
    }

    // Update audio parameters
    function updateAudio() {
      if (!audioContext) return;
      
      const now = audioContext.currentTime;
      // Carrier frequency is set by the sequencer, but we update modulator
      modulatorOsc.frequency.setTargetAtTime(params.modulatorFreq, now, 0.01);
    }

    // Update modulation gain (depends on both depth and modulator frequency)
    // depth=1 means one full cycle of phase modulation (2π radians)
    function updateModulationGain() {
      if (!audioContext || !modulationGain) return;
      
      const now = audioContext.currentTime;
      const gain = params.modulationDepth * 2 * Math.PI * params.modulatorFreq;
      modulationGain.gain.setTargetAtTime(gain, now, 0.01);
    }

    // Toggle play/stop
    function togglePlay() {
      const btn = document.getElementById('powerBtn');
      
      if (!isPlaying) {
        if (!audioContext) {
          initAudio();
        } else if (audioContext.state === 'suspended') {
          audioContext.resume();
        }
        masterGain.gain.setTargetAtTime(0.3, audioContext.currentTime, 0.05);
        btn.classList.add('active');
        btn.querySelector('span').textContent = '■ STOP';
        isPlaying = true;
        updateLFO(); // Start the LFO
        drawWaveform();
        animateLEDs();
      } else {
        stopLFO(); // Stop the LFO
        masterGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        envelopeGain.gain.setTargetAtTime(0, audioContext.currentTime, 0.05);
        btn.classList.remove('active');
        btn.querySelector('span').textContent = '▶ PLAY';
        isPlaying = false;
        currentStep = 0; // Reset sequencer
        updateStepLEDs();
      }
    }

    // Knob interaction
    function setupKnob(knobId) {
      const knob = document.getElementById(knobId);
      const config = knobConfig[knobId];
      let isDragging = false;
      let startY = 0;
      let startAngle = knobAngles[knobId];

      // Calculate initial angle from value (supports exponential scaling)
      const valueToAngle = (value) => {
        let normalized;
        if (config.exponential) {
          // Exponential: normalized = log(value/min) / log(max/min)
          normalized = Math.log(value / config.min) / Math.log(config.max / config.min);
        } else {
          normalized = (value - config.min) / (config.max - config.min);
        }
        return -135 + normalized * 270;
      };

      const angleToValue = (angle) => {
        const normalized = (angle + 135) / 270;
        if (config.exponential) {
          // Exponential: value = min * (max/min)^normalized
          return config.min * Math.pow(config.max / config.min, normalized);
        } else {
          return config.min + normalized * (config.max - config.min);
        }
      };

      // Set initial rotation
      knobAngles[knobId] = valueToAngle(config.value);
      knob.style.transform = `rotate(${knobAngles[knobId]}deg)`;

      const onMouseDown = (e) => {
        isDragging = true;
        startY = e.clientY || e.touches?.[0]?.clientY;
        startAngle = knobAngles[knobId];
        document.body.style.cursor = 'grabbing';
        e.preventDefault();
      };

      const onMouseMove = (e) => {
        if (!isDragging) return;
        
        const currentY = e.clientY || e.touches?.[0]?.clientY;
        const deltaY = startY - currentY;
        const sensitivity = 1.5;
        
        let newAngle = startAngle + deltaY * sensitivity;
        newAngle = Math.max(-135, Math.min(135, newAngle));
        
        knobAngles[knobId] = newAngle;
        knob.style.transform = `rotate(${newAngle}deg)`;
        
        const newValue = angleToValue(newAngle);
        config.value = newValue;
        
        // Handle step knobs differently - they store in stepOffsets array
        if (config.stepIndex !== undefined) {
          params.stepOffsets[config.stepIndex] = Math.round(newValue);
        } else {
          params[config.param] = newValue;
        }
        
        document.getElementById(config.valueEl).textContent = config.display(newValue);
        updateAudio();
        
        // Call onChange handler if defined (for LFO rate updates)
        if (config.onChange) {
          config.onChange();
        }
      };

      const onMouseUp = () => {
        isDragging = false;
        document.body.style.cursor = '';
      };

      knob.addEventListener('mousedown', onMouseDown);
      knob.addEventListener('touchstart', onMouseDown, { passive: false });
      
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('touchmove', onMouseMove, { passive: false });
      
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('touchend', onMouseUp);
    }

    // Waveform visualization
    function drawWaveform() {
      if (!isPlaying || !analyser) return;
      
      const canvas = document.getElementById('waveform');
      const ctx = canvas.getContext('2d');
      
      // Set canvas size
      canvas.width = canvas.offsetWidth * 2;
      canvas.height = canvas.offsetHeight * 2;
      
      const bufferLength = analyser.fftSize;
      const dataArray = new Uint8Array(bufferLength);
      
      function draw() {
        if (!isPlaying) {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          return;
        }
        
        requestAnimationFrame(draw);
        analyser.getByteTimeDomainData(dataArray);
        
        ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw waveform
        ctx.lineWidth = 3;
        ctx.strokeStyle = '#00f0ff';
        ctx.shadowBlur = 10;
        ctx.shadowColor = '#00f0ff';
        
        ctx.beginPath();
        
        const sliceWidth = canvas.width / bufferLength;
        let x = 0;
        
        for (let i = 0; i < bufferLength; i++) {
          const v = dataArray[i] / 128.0;
          const y = v * canvas.height / 2;
          
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
          
          x += sliceWidth;
        }
        
        ctx.stroke();
        
        // Draw center line
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
        ctx.shadowBlur = 0;
        ctx.beginPath();
        ctx.moveTo(0, canvas.height / 2);
        ctx.lineTo(canvas.width, canvas.height / 2);
        ctx.stroke();
      }
      
      draw();
    }

    // LED animation
    function animateLEDs() {
      const leds = document.querySelectorAll('.led');
      let currentLed = 0;
      
      function updateLEDs() {
        if (!isPlaying) {
          leds.forEach(led => led.classList.remove('active'));
          return;
        }
        
        leds.forEach((led, i) => {
          led.classList.toggle('active', i === currentLed);
        });
        
        currentLed = (currentLed + 1) % leds.length;
        setTimeout(updateLEDs, 100);
      }
      
      updateLEDs();
    }

    // Initialize
    document.addEventListener('DOMContentLoaded', () => {
      setupKnob('baseFreqKnob');
      setupKnob('modulatorKnob');
      setupKnob('depthKnob');
      setupKnob('decayKnob');
      setupKnob('lfoKnob');
      setupKnob('step0Knob');
      setupKnob('step1Knob');
      setupKnob('step2Knob');
      setupKnob('step3Knob');
      
      document.getElementById('powerBtn').addEventListener('click', togglePlay);
      
      // Initialize display values
      Object.keys(knobConfig).forEach(knobId => {
        const config = knobConfig[knobId];
        document.getElementById(config.valueEl).textContent = config.display(config.value);
      });
      
      // Initialize step LEDs
      updateStepLEDs();
    });
  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FM Synth</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --bg-dark: #0a0a0f;
      --panel-bg: #12121a;
      --accent-cyan: #00f0ff;
      --accent-magenta: #ff00aa;
      --accent-orange: #ff6b00;
      --accent-green: #00ff88;
      --accent-yellow: #ffee00;
      --accent-violet: #aa66ff;
      --text-dim: #4a4a5a;
      --text-bright: #e0e0e8;
      --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
      --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.5);
      --glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
      --glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
      --glow-yellow: 0 0 20px rgba(255, 238, 0, 0.5);
      --glow-violet: 0 0 20px rgba(170, 102, 255, 0.5);
    }

    body {
      min-height: 100vh;
      background: var(--bg-dark);
      font-family: 'Share Tech Mono', monospace;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      position: relative;
    }

    /* Animated background grid */
    body::before {
      content: '';
      position: absolute;
      inset: 0;
      background-image: 
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
      background-size: 50px 50px;
      animation: gridMove 20s linear infinite;
    }

    @keyframes gridMove {
      0% { transform: perspective(500px) rotateX(60deg) translateY(0); }
      100% { transform: perspective(500px) rotateX(60deg) translateY(50px); }
    }

    /* Gradient orbs in background */
    .orb {
      position: absolute;
      border-radius: 50%;
      filter: blur(80px);
      opacity: 0.4;
      animation: pulse 4s ease-in-out infinite alternate;
    }

    .orb-1 {
      width: 400px;
      height: 400px;
      background: radial-gradient(circle, var(--accent-cyan), transparent);
      top: -100px;
      left: -100px;
    }

    .orb-2 {
      width: 300px;
      height: 300px;
      background: radial-gradient(circle, var(--accent-magenta), transparent);
      bottom: -50px;
      right: -50px;
      animation-delay: 2s;
    }

    @keyframes pulse {
      0% { opacity: 0.3; transform: scale(1); }
      100% { opacity: 0.5; transform: scale(1.1); }
    }

    .synth-panel {
      position: relative;
      z-index: 1;
      background: linear-gradient(145deg, #15151f, #0d0d14);
      border: 1px solid rgba(0, 240, 255, 0.2);
      border-radius: 24px;
      padding: 48px 56px;
      box-shadow: 
        0 25px 80px rgba(0, 0, 0, 0.8),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    }

    .synth-panel::before {
      content: '';
      position: absolute;
      inset: -2px;
      border-radius: 26px;
      background: linear-gradient(135deg, var(--accent-cyan), var(--accent-magenta), var(--accent-orange));
      z-index: -1;
      opacity: 0.3;
      filter: blur(2px);
    }

    header {
      text-align: center;
      margin-bottom: 40px;
    }

    h1 {
      font-family: 'Orbitron', sans-serif;
      font-weight: 900;
      font-size: 2.5rem;
      letter-spacing: 0.3em;
      background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      text-shadow: var(--glow-cyan);
    }

    .subtitle {
      font-size: 0.75rem;
      color: var(--text-dim);
      letter-spacing: 0.5em;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      gap: 48px;
      justify-content: center;
      flex-wrap: wrap;
    }

    .control-section {
      margin-bottom: 32px;
    }

    .section-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.6rem;
      letter-spacing: 0.3em;
      color: var(--text-dim);
      text-align: center;
      margin-bottom: 20px;
      text-transform: uppercase;
    }

    .knob-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
    }

    .knob-label {
      font-family: 'Orbitron', sans-serif;
      font-size: 0.7rem;
      letter-spacing: 0.2em;
      text-transform: uppercase;
    }

    .knob-label.cyan { color: var(--accent-cyan); }
    .knob-label.magenta { color: var(--accent-magenta); }
    .knob-label.orange { color: var(--accent-orange); }
    .knob-label.green { color: var(--accent-green); }
    .knob-label.yellow { color: var(--accent-yellow); }
    .knob-label.violet { color: var(--accent-violet); }

    .knob-wrapper {
      position: relative;
      width: 120px;
      height: 120px;
    }

    .knob {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      background: linear-gradient(145deg, #1a1a24, #0e0e14);
      border: 2px solid rgba(255, 255, 255, 0.1);
      cursor: pointer;
      position: relative;
      transition: transform 0.1s ease;
      box-shadow: 
        inset 0 -4px 8px rgba(0, 0, 0, 0.5),
        0 4px 12px rgba(0, 0, 0, 0.5);
    }

    .knob::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      width: 60%;
      height: 60%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      background: linear-gradient(145deg, #0e0e14, #1a1a24);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    .knob-indicator {
      position: absolute;
      top: 12%;
      left: 50%;
      width: 4px;
      height: 16px;
      transform: translateX(-50%);
      border-radius: 2px;
    }

    .knob-indicator.cyan { background: var(--accent-cyan); box-shadow: var(--glow-cyan); }
    .knob-indicator.magenta { background: var(--accent-magenta); box-shadow: var(--glow-magenta); }
    .knob-indicator.orange { background: var(--accent-orange); box-shadow: var(--glow-orange); }
    .knob-indicator.green { background: var(--accent-green); box-shadow: var(--glow-green); }
    .knob-indicator.yellow { background: var(--accent-yellow); box-shadow: var(--glow-yellow); }
    .knob-indicator.violet { background: var(--accent-violet); box-shadow: var(--glow-violet); }

    .knob-ring {
      position: absolute;
      inset: -8px;
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment