{"product_id":"moonlight-vocal-mixing-suite","title":"Moonlight Vocal Mixing Suite","description":"\u003cdiv class=\"audio-demo-block\"\u003e\n\n  \u003cdiv class=\"unified-player\"\u003e\n\n    \u003cdiv class=\"player-top\"\u003e\n\n      \u003cdiv class=\"player-left\"\u003e\n        \u003cbutton type=\"button\" class=\"play-btn\"\u003e▶\u003c\/button\u003e\n\n        \u003cdiv class=\"player-meta\"\u003e\n          \u003cdiv class=\"player-kicker\"\u003eA\/B VOCAL DEMO\u003c\/div\u003e\n          \u003cdiv class=\"example-title\"\u003eExample 1\u003c\/div\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"ab-toggle\"\u003e\n        \u003cspan\u003eBEFORE\u003c\/span\u003e\n\n        \u003clabel class=\"switch\"\u003e\n          \u003cinput class=\"toggle\" type=\"checkbox\"\u003e\n          \u003cspan class=\"slider\"\u003e\u003c\/span\u003e\n        \u003c\/label\u003e\n\n        \u003cspan\u003eAFTER\u003c\/span\u003e\n      \u003c\/div\u003e\n\n    \u003c\/div\u003e\n\n    \u003cdiv class=\"progress-wrap\"\u003e\n      \u003cinput step=\"0.1\" max=\"100\" min=\"0\" value=\"0\" class=\"progress-bar\" type=\"range\"\u003e\n    \u003c\/div\u003e\n\n  \u003c\/div\u003e\n\n\n  \u003cdiv class=\"demo-list\"\u003e\n\n    \u003cbutton type=\"button\" class=\"demo-item active\" data-before=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/2_-_Sakhile_-_Fall_into_my_arms_RAW.mp3?v=1783882214\" data-after=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/2_-_Sakhile_-_Fall_into_my_arms_WET.mp3?v=1783882214\"\u003e\n      \u003cspan class=\"demo-index\"\u003e01\u003c\/span\u003e\n      \u003cspan class=\"demo-text\"\u003e\n        \u003cspan class=\"demo-name\"\u003eExample 1\u003c\/span\u003e\n      \u003c\/span\u003e\n      \u003cspan class=\"demo-state\"\u003eSelected\u003c\/span\u003e\n    \u003c\/button\u003e\n\n\n    \u003cbutton type=\"button\" class=\"demo-item\" data-before=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/3_-_Sakhile_-_Time_Flies_RAW.mp3?v=1783882227\" data-after=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/3_-_Sakhile_-_Time_Flies_WET.mp3?v=1783882227\"\u003e\n      \u003cspan class=\"demo-index\"\u003e02\u003c\/span\u003e\n      \u003cspan class=\"demo-text\"\u003e\n        \u003cspan class=\"demo-name\"\u003eExample 2\u003c\/span\u003e\n      \u003c\/span\u003e\n      \u003cspan class=\"demo-state\"\u003eSelect\u003c\/span\u003e\n    \u003c\/button\u003e\n\n\n    \u003cbutton type=\"button\" class=\"demo-item\" data-before=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/15_-_Senpaii_Call_me_rn_RAW.mp3?v=1783882258\" data-after=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/15_-_Senpaii_Call_me_rn_WET.mp3?v=1783882257\"\u003e\n      \u003cspan class=\"demo-index\"\u003e03\u003c\/span\u003e\n      \u003cspan class=\"demo-text\"\u003e\n        \u003cspan class=\"demo-name\"\u003eExample 3\u003c\/span\u003e\n      \u003c\/span\u003e\n      \u003cspan class=\"demo-state\"\u003eSelect\u003c\/span\u003e\n    \u003c\/button\u003e\n\n\n    \u003cbutton type=\"button\" class=\"demo-item\" data-before=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/4_-_Sakhile_Looking_back_RAW.mp3?v=1783882234\" data-after=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/4_-_Sakhile_Looking_back_WET.mp3?v=1783882235\"\u003e\n      \u003cspan class=\"demo-index\"\u003e04\u003c\/span\u003e\n      \u003cspan class=\"demo-text\"\u003e\n        \u003cspan class=\"demo-name\"\u003eExample 4\u003c\/span\u003e\n      \u003c\/span\u003e\n      \u003cspan class=\"demo-state\"\u003eSelect\u003c\/span\u003e\n    \u003c\/button\u003e\n\n\n    \u003cbutton type=\"button\" class=\"demo-item\" data-before=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/10_-_Senpaii_RnB_RAW.mp3?v=1783882245\" data-after=\"https:\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/10_-_Senpaii_RnB_WET.mp3?v=1783882245\"\u003e\n      \u003cspan class=\"demo-index\"\u003e05\u003c\/span\u003e\n      \u003cspan class=\"demo-text\"\u003e\n        \u003cspan class=\"demo-name\"\u003eExample 5\u003c\/span\u003e\n      \u003c\/span\u003e\n      \u003cspan class=\"demo-state\"\u003eSelect\u003c\/span\u003e\n    \u003c\/button\u003e\n\n  \u003c\/div\u003e\n\n\u003c\/div\u003e\n\n\n\u003cstyle\u003e\n\n.audio-demo-block{\n  max-width:720px;\n  margin:20px auto;\n  display:flex;\n  flex-direction:column;\n  gap:12px;\n}\n\n.unified-player{\n  display:flex;\n  flex-direction:column;\n  gap:12px;\n  padding:14px 16px;\n  background:#000;\n  border-radius:12px;\n  color:#fff;\n  width:100%;\n  box-sizing:border-box;\n  box-shadow:0 4px 20px rgba(0,0,0,0.25);\n}\n\n.unified-player.playing{\n  box-shadow:\n    0 4px 20px rgba(0,0,0,0.25),\n    0 0 20px rgba(160,0,255,0.18);\n}\n\n.player-top{\n  display:flex;\n  justify-content:space-between;\n  align-items:center;\n  gap:12px;\n  flex-wrap:wrap;\n}\n\n.player-left{\n  display:flex;\n  align-items:center;\n  gap:12px;\n  min-width:0;\n}\n\n.player-meta{\n  display:flex;\n  flex-direction:column;\n  gap:2px;\n  min-width:0;\n}\n\n.player-kicker{\n  font-size:10px;\n  letter-spacing:.12em;\n  text-transform:uppercase;\n  color:#9b9b9b;\n}\n\n.play-btn{\n  width:38px;\n  height:38px;\n  min-width:38px;\n  border:none;\n  border-radius:50%;\n  background:#fff;\n  color:#111;\n  cursor:pointer;\n  font-size:14px;\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  line-height:1;\n}\n\n.play-btn.loading{\n  opacity:.65;\n  cursor:wait;\n}\n\n.example-title{\n  font-size:14px;\n  font-weight:600;\n  color:#fff;\n  margin:0;\n  white-space:nowrap;\n  overflow:hidden;\n  text-overflow:ellipsis;\n}\n\n.ab-toggle{\n  display:flex;\n  align-items:center;\n  gap:8px;\n  font-size:11px;\n  color:#d6d6d6;\n}\n\n.switch{\n  position:relative;\n  width:38px;\n  height:20px;\n}\n\n.switch input{\n  display:none;\n}\n\n.slider{\n  position:absolute;\n  inset:0;\n  background:#555;\n  border-radius:20px;\n  cursor:pointer;\n  transition:background .2s ease;\n}\n\n.slider:before{\n  content:\"\";\n  position:absolute;\n  width:14px;\n  height:14px;\n  left:3px;\n  bottom:3px;\n  background:#fff;\n  border-radius:50%;\n  transition:.2s;\n}\n\n.toggle:checked + .slider{\n  background:#A000FF;\n}\n\n.toggle:checked + .slider:before{\n  transform:translateX(18px);\n}\n\n.progress-wrap{\n  width:100%;\n}\n\n.progress-bar{\n  -webkit-appearance:none;\n  appearance:none;\n  width:100%;\n  height:6px;\n  border-radius:999px;\n  cursor:pointer;\n  outline:none;\n  background:linear-gradient(\n    90deg,\n    #140028 0%,\n    #5A00CC calc(var(--progress, 0%) \/ 2),\n    #A000FF var(--progress, 0%),\n    #2a2a2a var(--progress, 0%),\n    #2a2a2a 100%\n  );\n}\n\n.progress-bar::-webkit-slider-runnable-track{\n  height:6px;\n  border-radius:999px;\n  background:transparent;\n}\n\n.progress-bar::-webkit-slider-thumb{\n  -webkit-appearance:none;\n  appearance:none;\n  width:12px;\n  height:12px;\n  border-radius:50%;\n  background:#A000FF;\n  border:none;\n  margin-top:-3px;\n  box-shadow:\n    0 0 10px #A000FF,\n    0 0 20px rgba(160,0,255,0.45);\n}\n\n.progress-bar::-moz-range-track{\n  height:6px;\n  border-radius:999px;\n  background:transparent;\n}\n\n.progress-bar::-moz-range-thumb{\n  width:12px;\n  height:12px;\n  border-radius:50%;\n  background:#A000FF;\n  border:none;\n  box-shadow:\n    0 0 10px #A000FF,\n    0 0 20px rgba(160,0,255,0.45);\n}\n\n.demo-list{\n  display:flex;\n  flex-direction:column;\n  gap:8px;\n}\n\n.demo-item{\n  width:100%;\n  border:none;\n  background:#0b0b0b;\n  color:#fff;\n  padding:14px 16px;\n  border-radius:12px;\n  display:grid;\n  grid-template-columns:42px minmax(0,1fr) auto;\n  align-items:center;\n  gap:12px;\n  text-align:left;\n  cursor:pointer;\n  transition:\n    transform .18s ease,\n    background .18s ease,\n    box-shadow .18s ease;\n  box-shadow:0 2px 10px rgba(0,0,0,0.12);\n}\n\n.demo-item:hover{\n  transform:translateY(-1px);\n  background:#111;\n}\n\n.demo-item.active{\n  background:linear-gradient(\n    90deg,\n    rgba(20,0,40,1) 0%,\n    rgba(90,0,204,0.95) 55%,\n    rgba(160,0,255,0.95) 100%\n  );\n\n  box-shadow:0 0 14px rgba(160,0,255,0.18);\n}\n\n.demo-index{\n  font-size:12px;\n  font-weight:700;\n  letter-spacing:.08em;\n  color:#cfcfcf;\n}\n\n.demo-text{\n  min-width:0;\n  display:flex;\n  align-items:center;\n}\n\n.demo-name{\n  font-size:14px;\n  font-weight:600;\n  color:#fff;\n}\n\n.demo-state{\n  font-size:11px;\n  font-weight:700;\n  color:#fff;\n  opacity:.9;\n}\n\n.demo-item:not(.active) .demo-state{\n  color:#b9b9b9;\n}\n\n@media (max-width:749px){\n\n  .audio-demo-block{\n    margin:16px auto;\n  }\n\n  .player-top{\n    align-items:flex-start;\n  }\n\n  .ab-toggle{\n    margin-left:0;\n  }\n\n  .demo-item{\n    grid-template-columns:34px minmax(0,1fr);\n  }\n\n  .demo-state{\n    display:none;\n  }\n\n  .example-title{\n    white-space:normal;\n    overflow:visible;\n    text-overflow:unset;\n  }\n\n}\n\n\u003c\/style\u003e\n\n\n\u003cscript\u003e\n\ndocument.addEventListener('DOMContentLoaded', function(){\n\n  document\n    .querySelectorAll('.audio-demo-block')\n    .forEach(function(block){\n\n      \/* ========================================\n         ELEMENTE\n      ======================================== *\/\n\n      const player =\n        block.querySelector('.unified-player');\n\n      const playBtn =\n        block.querySelector('.play-btn');\n\n      const toggle =\n        block.querySelector('.toggle');\n\n      const progress =\n        block.querySelector('.progress-bar');\n\n      const title =\n        block.querySelector('.example-title');\n\n      const demoItems =\n        Array.from(\n          block.querySelectorAll('.demo-item')\n        );\n\n\n      \/* ========================================\n         WEB AUDIO API\n      ======================================== *\/\n\n      const AudioContextClass =\n        window.AudioContext ||\n        window.webkitAudioContext;\n\n      const audioContext =\n        new AudioContextClass();\n\n\n      \/*\n         Zwei Lautstärkeregler.\n\n         BEFORE:\n         1 = hörbar\n\n         AFTER:\n         0 = stumm\n      *\/\n\n      const beforeGain =\n        audioContext.createGain();\n\n      const afterGain =\n        audioContext.createGain();\n\n      beforeGain.connect(\n        audioContext.destination\n      );\n\n      afterGain.connect(\n        audioContext.destination\n      );\n\n      beforeGain.gain.value = 1;\n      afterGain.gain.value = 0;\n\n\n      \/* ========================================\n         STATUS\n      ======================================== *\/\n\n      let currentItem =\n        block.querySelector('.demo-item.active');\n\n      let beforeBuffer = null;\n      let afterBuffer = null;\n\n      let beforeSource = null;\n      let afterSource = null;\n\n      let playing = false;\n      let loading = false;\n\n      \/*\n         Position im Lied,\n         an der Play gestartet wurde.\n      *\/\n\n      let startOffset = 0;\n\n      \/*\n         AudioContext-Zeitpunkt,\n         an dem beide Tracks gestartet sind.\n      *\/\n\n      let startContextTime = 0;\n\n      let animationFrame = null;\n\n      let loadGeneration = 0;\n\n      let playbackGeneration = 0;\n\n\n      \/* ========================================\n         AUDIO CACHE\n\n         Jede Datei wird nur EINMAL geladen\n         und vollständig decodiert.\n      ======================================== *\/\n\n      const audioCache =\n        new Map();\n\n\n      async function loadAudio(url){\n\n        if(audioCache.has(url)){\n          return audioCache.get(url);\n        }\n\n\n        const loadingPromise =\n          fetch(url)\n            .then(function(response){\n\n              if(!response.ok){\n\n                throw new Error(\n                  'Audio konnte nicht geladen werden'\n                );\n\n              }\n\n              return response.arrayBuffer();\n\n            })\n            .then(function(arrayBuffer){\n\n              return audioContext.decodeAudioData(\n                arrayBuffer\n              );\n\n            });\n\n\n        audioCache.set(\n          url,\n          loadingPromise\n        );\n\n\n        return loadingPromise;\n\n      }\n\n\n      \/* ========================================\n         ALLE BEFORE \/ AFTER DATEIEN\n         DIREKT IM HINTERGRUND VORLADEN\n      ======================================== *\/\n\n      demoItems.forEach(function(item){\n\n        loadAudio(\n          item.dataset.before\n        ).catch(function(error){\n\n          console.error(\n            'Preload BEFORE:',\n            error\n          );\n\n        });\n\n\n        loadAudio(\n          item.dataset.after\n        ).catch(function(error){\n\n          console.error(\n            'Preload AFTER:',\n            error\n          );\n\n        });\n\n      });\n\n\n      \/* ========================================\n         A\/B SCHALTER\n\n         HIER PASSIERT ABSICHTLICH NICHTS\n         MIT PLAY \/ PAUSE \/ CURRENTTIME.\n\n         NUR:\n\n         BEFORE 1 \/ AFTER 0\n\n         oder\n\n         BEFORE 0 \/ AFTER 1\n      ======================================== *\/\n\n      function updateAB(){\n\n        const now =\n          audioContext.currentTime;\n\n\n        if(toggle.checked){\n\n          beforeGain.gain.setValueAtTime(\n            0,\n            now\n          );\n\n          afterGain.gain.setValueAtTime(\n            1,\n            now\n          );\n\n        }else{\n\n          beforeGain.gain.setValueAtTime(\n            1,\n            now\n          );\n\n          afterGain.gain.setValueAtTime(\n            0,\n            now\n          );\n\n        }\n\n      }\n\n\n      toggle.addEventListener(\n        'change',\n        function(){\n\n          updateAB();\n\n        }\n      );\n\n\n      \/* ========================================\n         PLAY BUTTON UI\n      ======================================== *\/\n\n      function updatePlayButton(){\n\n        playBtn.classList.toggle(\n          'loading',\n          loading\n        );\n\n\n        if(loading){\n\n          playBtn.textContent = '•••';\n\n          player.classList.remove(\n            'playing'\n          );\n\n          return;\n\n        }\n\n\n        if(playing){\n\n          playBtn.textContent = '❚❚';\n\n          player.classList.add(\n            'playing'\n          );\n\n        }else{\n\n          playBtn.textContent = '▶';\n\n          player.classList.remove(\n            'playing'\n          );\n\n        }\n\n      }\n\n\n      \/* ========================================\n         SOURCES STOPPEN\n      ======================================== *\/\n\n      function stopSources(){\n\n        playbackGeneration++;\n\n\n        if(beforeSource){\n\n          beforeSource.onended = null;\n\n          try{\n            beforeSource.stop();\n          }catch(e){}\n\n          try{\n            beforeSource.disconnect();\n          }catch(e){}\n\n          beforeSource = null;\n\n        }\n\n\n        if(afterSource){\n\n          afterSource.onended = null;\n\n          try{\n            afterSource.stop();\n          }catch(e){}\n\n          try{\n            afterSource.disconnect();\n          }catch(e){}\n\n          afterSource = null;\n\n        }\n\n      }\n\n\n      \/* ========================================\n         TRACK LÄNGE\n\n         Beide sind laut dir gleich lang.\n         Trotzdem nehmen wir sicherheitshalber\n         die kürzere Dauer.\n      ======================================== *\/\n\n      function getDuration(){\n\n        if(\n          !beforeBuffer ||\n          !afterBuffer\n        ){\n          return 0;\n        }\n\n\n        return Math.min(\n          beforeBuffer.duration,\n          afterBuffer.duration\n        );\n\n      }\n\n\n      \/* ========================================\n         AKTUELLE POSITION\n\n         KEIN currentTime DER AUDIODATEIEN.\n\n         Position wird nur mathematisch aus der\n         AudioContext-Uhr berechnet.\n      ======================================== *\/\n\n      function getCurrentPosition(){\n\n        if(!playing){\n          return startOffset;\n        }\n\n\n        const elapsed =\n          audioContext.currentTime -\n          startContextTime;\n\n\n        return Math.max(\n          0,\n          startOffset + elapsed\n        );\n\n      }\n\n\n      \/* ========================================\n         BEIDE TRACKS SAMPLEGENAU\n         GLEICHZEITIG STARTEN\n      ======================================== *\/\n\n      function startSources(offset){\n\n        if(\n          !beforeBuffer ||\n          !afterBuffer\n        ){\n          return;\n        }\n\n\n        stopSources();\n\n\n        const duration =\n          getDuration();\n\n\n        offset = Math.max(\n          0,\n          Math.min(\n            offset,\n            Math.max(\n              0,\n              duration - 0.001\n            )\n          )\n        );\n\n\n        \/*\n           Neue AudioBufferSources.\n\n           AudioBufferSourceNode kann nur\n           einmal gestartet werden.\n        *\/\n\n        beforeSource =\n          audioContext.createBufferSource();\n\n        afterSource =\n          audioContext.createBufferSource();\n\n\n        beforeSource.buffer =\n          beforeBuffer;\n\n        afterSource.buffer =\n          afterBuffer;\n\n\n        beforeSource.connect(\n          beforeGain\n        );\n\n        afterSource.connect(\n          afterGain\n        );\n\n\n        \/*\n           DAS IST DER WICHTIGE TEIL:\n\n           Beide Tracks erhalten exakt denselben\n           Startzeitpunkt der AudioContext Clock.\n        *\/\n\n        const scheduledStart =\n          audioContext.currentTime + 0.05;\n\n\n        startOffset =\n          offset;\n\n        startContextTime =\n          scheduledStart;\n\n\n        \/*\n           BEFORE \/ AFTER Lautstärke setzen,\n           BEVOR beide Tracks starten.\n        *\/\n\n        updateAB();\n\n\n        const thisPlayback =\n          ++playbackGeneration;\n\n\n        \/*\n           EXAKT SELBER STARTZEITPUNKT\n        *\/\n\n        beforeSource.start(\n          scheduledStart,\n          offset\n        );\n\n        afterSource.start(\n          scheduledStart,\n          offset\n        );\n\n\n        playing = true;\n\n        updatePlayButton();\n\n\n        \/*\n           Ende anhand eines Tracks erkennen.\n\n           Es wird hier NICHT synchronisiert.\n        *\/\n\n        beforeSource.onended =\n          function(){\n\n            if(\n              thisPlayback !==\n              playbackGeneration\n            ){\n              return;\n            }\n\n\n            if(!playing){\n              return;\n            }\n\n\n            const position =\n              getCurrentPosition();\n\n\n            if(\n              position \u003e=\n              duration - 0.1\n            ){\n\n              finishPlayback();\n\n            }\n\n          };\n\n\n        startProgressLoop();\n\n      }\n\n\n      \/* ========================================\n         PLAY\n      ======================================== *\/\n\n      async function startPlayback(){\n\n        if(\n          loading ||\n          !currentItem\n        ){\n          return;\n        }\n\n\n        loading = true;\n\n        updatePlayButton();\n\n\n        const generation =\n          ++loadGeneration;\n\n\n        try{\n\n          \/*\n             Browser verlangen normalerweise\n             einen User-Klick, bevor AudioContext\n             hörbar werden darf.\n          *\/\n\n          await audioContext.resume();\n\n\n          \/*\n             BEFORE UND AFTER vollständig laden.\n          *\/\n\n          const buffers =\n            await Promise.all([\n\n              loadAudio(\n                currentItem.dataset.before\n              ),\n\n              loadAudio(\n                currentItem.dataset.after\n              )\n\n            ]);\n\n\n          if(\n            generation !==\n            loadGeneration\n          ){\n            return;\n          }\n\n\n          beforeBuffer =\n            buffers[0];\n\n          afterBuffer =\n            buffers[1];\n\n\n          loading = false;\n\n\n          startSources(\n            startOffset\n          );\n\n\n        }catch(error){\n\n          if(\n            generation !==\n            loadGeneration\n          ){\n            return;\n          }\n\n\n          loading = false;\n          playing = false;\n\n          updatePlayButton();\n\n\n          console.error(\n            'Audio Fehler:',\n            error\n          );\n\n        }\n\n      }\n\n\n      \/* ========================================\n         PAUSE\n      ======================================== *\/\n\n      function pausePlayback(){\n\n        if(!playing){\n          return;\n        }\n\n\n        \/*\n           Position merken.\n        *\/\n\n        startOffset =\n          Math.min(\n            getCurrentPosition(),\n            getDuration()\n          );\n\n\n        playing = false;\n\n\n        stopSources();\n\n\n        if(animationFrame){\n\n          cancelAnimationFrame(\n            animationFrame\n          );\n\n          animationFrame = null;\n\n        }\n\n\n        updatePlayButton();\n\n      }\n\n\n      \/* ========================================\n         PLAY \/ PAUSE KLICK\n      ======================================== *\/\n\n      playBtn.addEventListener(\n        'click',\n        async function(){\n\n          if(loading){\n            return;\n          }\n\n\n          await audioContext.resume();\n\n\n          if(playing){\n\n            pausePlayback();\n\n          }else{\n\n            \/*\n               Falls Track komplett am Ende ist,\n               wieder bei 0 starten.\n            *\/\n\n            if(\n              getDuration() \u0026\u0026\n              startOffset \u003e=\n              getDuration() - 0.05\n            ){\n\n              startOffset = 0;\n\n            }\n\n\n            startPlayback();\n\n          }\n\n        }\n      );\n\n\n      \/* ========================================\n         PROGRESS BAR\n      ======================================== *\/\n\n      function updateProgress(){\n\n        const duration =\n          getDuration();\n\n\n        if(!duration){\n          return;\n        }\n\n\n        const position =\n          Math.min(\n            getCurrentPosition(),\n            duration\n          );\n\n\n        const percent =\n          Math.min(\n            100,\n            Math.max(\n              0,\n              position \/\n              duration *\n              100\n            )\n          );\n\n\n        progress.value =\n          percent;\n\n\n        progress.style.setProperty(\n          '--progress',\n          percent + '%'\n        );\n\n      }\n\n\n      function progressLoop(){\n\n        if(!playing){\n          return;\n        }\n\n\n        updateProgress();\n\n\n        \/*\n           Falls onended im Browser einmal\n           verspätet kommt, hier zusätzlich prüfen.\n        *\/\n\n        const duration =\n          getDuration();\n\n\n        if(\n          duration \u0026\u0026\n          getCurrentPosition() \u003e=\n          duration\n        ){\n\n          finishPlayback();\n\n          return;\n\n        }\n\n\n        animationFrame =\n          requestAnimationFrame(\n            progressLoop\n          );\n\n      }\n\n\n      function startProgressLoop(){\n\n        if(animationFrame){\n\n          cancelAnimationFrame(\n            animationFrame\n          );\n\n        }\n\n\n        animationFrame =\n          requestAnimationFrame(\n            progressLoop\n          );\n\n      }\n\n\n      \/* ========================================\n         SEEK \/ PROGRESS ZIEHEN\n\n         Beim Ziehen wird nur die Anzeige\n         verändert.\n\n         Erst beim Loslassen werden BEIDE\n         gemeinsam neu gestartet.\n      ======================================== *\/\n\n      progress.addEventListener(\n        'input',\n        function(){\n\n          const percent =\n            parseFloat(\n              progress.value\n            );\n\n\n          progress.style.setProperty(\n            '--progress',\n            percent + '%'\n          );\n\n        }\n      );\n\n\n      function seekToProgress(){\n\n        const duration =\n          getDuration();\n\n\n        if(!duration){\n          return;\n        }\n\n\n        const percent =\n          parseFloat(\n            progress.value\n          );\n\n\n        const newPosition =\n          duration *\n          (\n            percent \/ 100\n          );\n\n\n        startOffset =\n          newPosition;\n\n\n        \/*\n           Nur beim SEEK müssen die Sources\n           neu erzeugt werden.\n\n           Beide starten danach aber wieder\n           exakt gleichzeitig.\n        *\/\n\n        if(playing){\n\n          startSources(\n            newPosition\n          );\n\n        }else{\n\n          updateProgress();\n\n        }\n\n      }\n\n\n      progress.addEventListener(\n        'change',\n        seekToProgress\n      );\n\n\n      \/* ========================================\n         TRACK ENDE\n      ======================================== *\/\n\n      function finishPlayback(){\n\n        if(!playing){\n          return;\n        }\n\n\n        playing = false;\n\n\n        stopSources();\n\n\n        startOffset = 0;\n\n\n        if(animationFrame){\n\n          cancelAnimationFrame(\n            animationFrame\n          );\n\n          animationFrame = null;\n\n        }\n\n\n        progress.value = 100;\n\n\n        progress.style.setProperty(\n          '--progress',\n          '100%'\n        );\n\n\n        updatePlayButton();\n\n      }\n\n\n      \/* ========================================\n         EXAMPLE AUSWÄHLEN\n      ======================================== *\/\n\n      async function selectDemo(item){\n\n        if(\n          item ===\n          currentItem\n        ){\n          return;\n        }\n\n\n        const generation =\n          ++loadGeneration;\n\n\n        \/*\n           Aktuellen Track stoppen.\n        *\/\n\n        playing = false;\n\n        stopSources();\n\n\n        if(animationFrame){\n\n          cancelAnimationFrame(\n            animationFrame\n          );\n\n          animationFrame = null;\n\n        }\n\n\n        loading = true;\n\n        updatePlayButton();\n\n\n        currentItem =\n          item;\n\n\n        \/*\n           Neues Beispiel immer auf BEFORE.\n        *\/\n\n        toggle.checked = false;\n\n        updateAB();\n\n\n        \/* ========================\n           UI\n        ======================== *\/\n\n        demoItems.forEach(\n          function(button){\n\n            button.classList.remove(\n              'active'\n            );\n\n\n            const state =\n              button.querySelector(\n                '.demo-state'\n              );\n\n\n            if(state){\n\n              state.textContent =\n                'Select';\n\n            }\n\n          }\n        );\n\n\n        item.classList.add(\n          'active'\n        );\n\n\n        const selectedState =\n          item.querySelector(\n            '.demo-state'\n          );\n\n\n        if(selectedState){\n\n          selectedState.textContent =\n            'Selected';\n\n        }\n\n\n        const demoName =\n          item.querySelector(\n            '.demo-name'\n          );\n\n\n        if(demoName){\n\n          title.textContent =\n            demoName.textContent;\n\n        }\n\n\n        \/*\n           Neues Beispiel beginnt bei 0.\n        *\/\n\n        startOffset = 0;\n\n        progress.value = 0;\n\n        progress.style.setProperty(\n          '--progress',\n          '0%'\n        );\n\n\n        try{\n\n          await audioContext.resume();\n\n\n          \/*\n             Beide Dateien komplett aus dem Cache\n             holen bzw. fertig laden.\n          *\/\n\n          const buffers =\n            await Promise.all([\n\n              loadAudio(\n                item.dataset.before\n              ),\n\n              loadAudio(\n                item.dataset.after\n              )\n\n            ]);\n\n\n          if(\n            generation !==\n            loadGeneration\n          ){\n            return;\n          }\n\n\n          beforeBuffer =\n            buffers[0];\n\n          afterBuffer =\n            buffers[1];\n\n\n          loading = false;\n\n\n          \/*\n             Neues Example automatisch starten.\n\n             BEFORE und AFTER starten dabei\n             exakt gleichzeitig.\n          *\/\n\n          startSources(0);\n\n\n        }catch(error){\n\n          if(\n            generation !==\n            loadGeneration\n          ){\n            return;\n          }\n\n\n          loading = false;\n          playing = false;\n\n          updatePlayButton();\n\n\n          console.error(\n            'Audio Fehler:',\n            error\n          );\n\n        }\n\n      }\n\n\n      \/* ========================================\n         EXAMPLE BUTTONS\n      ======================================== *\/\n\n      demoItems.forEach(\n        function(item){\n\n          item.addEventListener(\n            'click',\n            function(){\n\n              selectDemo(item);\n\n            }\n          );\n\n        }\n      );\n\n\n      \/* ========================================\n         ERSTES EXAMPLE KOMPLETT VORLADEN\n\n         NOCH NICHT ABSPIELEN.\n      ======================================== *\/\n\n      if(currentItem){\n\n        Promise.all([\n\n          loadAudio(\n            currentItem.dataset.before\n          ),\n\n          loadAudio(\n            currentItem.dataset.after\n          )\n\n        ])\n        .then(function(buffers){\n\n          beforeBuffer =\n            buffers[0];\n\n          afterBuffer =\n            buffers[1];\n\n        })\n        .catch(function(error){\n\n          console.error(\n            'Initial Preload Fehler:',\n            error\n          );\n\n        });\n\n      }\n\n\n      \/* ========================================\n         INITIALER STATUS\n      ======================================== *\/\n\n      toggle.checked = false;\n\n      updateAB();\n\n      updatePlayButton();\n\n    });\n\n});\n\n\u003c\/script\u003e","brand":"eqlipse.studio","offers":[{"title":"FL Studio","offer_id":56437085045076,"sku":"2.1","price":79.0,"currency_code":"EUR","in_stock":true},{"title":"Logic Pro","offer_id":56437085077844,"sku":"2.2","price":79.0,"currency_code":"EUR","in_stock":false},{"title":"BandLab","offer_id":56437085110612,"sku":"2.3","price":79.0,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/1052\/4950\/5620\/files\/Moonlight_VocalMixingSuite_frontal_querformat_84f6f2da-9f48-4585-b895-0739d0f06099.png?v=1786987156","url":"https:\/\/eqlipse.studio\/products\/moonlight-vocal-mixing-suite","provider":"eqlipse.studio","version":"1.0","type":"link"}