MrPowerShell

Audio Visualizer

Options
    Audio
      Rate
    Visuals
      Palette
      Scopes
      Pattern
      Bars
      CSS
    View Source
              
      #requires -Module Turtle 
      
      if (-not $Page) {
          $Page = [Ordered]@{}
      }
      
      $title = "Audio Visualizer"
      
      $description = "A simple audio visualizer using the Web Audio API, made with PowerShell."
      
      if ($Page) {
          $page.Title = $title
          $Page.Description = $description
          $Page.Image = "https://MrPowerShell.com/HTML/AudioVisualizerInColor.png"
          # The page background is randomly selected during site configuration.    
      }
      
      $randomPalette = @"
      <script>
      function SetRandomPalette() {
          var SelectPalette = document.getElementById('SelectPalette')
          var randomNumber = Math.floor(Math.random() * SelectPalette.length);
          SelectPalette.selectedIndex = randomNumber
          SetPalette()
      }
      </script>
      "@
      
      $randomColor = @"
      <script>
      function SetRandomColor() {
          var SelectColor = document.getElementById('SelectColor')
          if (!SelectColor) { return }
          var randomNumber = Math.floor(Math.random() * SelectColor.length)
          SelectColor.selectedIndex = randomNumber
      }
      </script>
      "@
      
      $savePng = @"
      <script>
      function SavePNG(elementId) {
          var canvas = document.getElementById(elementId)
          var dataURL = canvas.toDataURL('image/png')
          var a = document.createElement('a')
          a.href = dataURL
          a.download = ```${elementId}.png``
          a.click()
          console.log('Saved PNG')
      }
      </script>
      "@
      
      $OnResize = '
      <script>
      function Resize() {    
          var visuals = document.getElementById("visuals")    
          var powerShellCode = document.getElementById("PowerShellCode")
          if (window.innerWidth) {        
              //visuals.width = window.innerWidth
              //visuals.height = window.innerHeight * 0.7
              powerShellCode.style.top = window.innerHeight
          } else {
              //visuals.width = screen.width
              //visuals.height = screen.height * 0.7
              powerShellCode.style.top = screen.height            
          }
              
          console.log(`Resized ${screen.width}x${screen.height}`)
      }
      window.addEventListener("resize", function() {
          Resize()
      })
      Resize()
      </script>
      '
      
      $colorSelector = @"
      <select id='SelectColor' selected='foreground'>
      $(foreach ($colorName in 'foreground','red','green','blue','yellow','purple','cyan','brightBlue','brightRed','brightGreen','brightYellow','brightPurple','brightCyan') {
          "<option value='--$colorName'>$colorName</option>"    
      })
      </select>
      "@
      
      $randomPalette
      $randomColor
      $savePng
      
      
      $Style = @"
      .invisible { display: none; }
      
      .overlay { z-index: 50 }
      
      .visualsGrid {
          position: absolute;
          z-index: -1;
          top: 0;
          left: 0;
          display: grid;
          width: 100vw;
          height: 100vh;
      }
      
      .audioFieldset {
          display: grid;
          width: 29ch;
          grid-template-areas:
              'levelsAndPan'
              'rateAndPitch';
      }
      
      input[type="file"]::file-selector-button {
          text-align: center;
          color: var(--foreground);
          background-color: var(--background);
          border: thin solid var(--foreground);
          border-radius: 0.25rem;
      }
      
      .rateAndPitch { grid-area: rateAndPitch; display: grid; }
      
      .levelsAndPanGrid {
          display: grid;
          grid-area: 'levelsAndPan'
          text-align: center;
          width: 12ch;
          margin-left:auto;
          margin-right: auto;
          grid-template-areas: 
              'leftGain rightGain'
              'leftLabel rightLabel'
              'panInput panInput'
              'panLabel panLabel'
          ;
      }
      
      .leftGainInput { grid-area: leftGain }
      .rightGainInput { grid-area: rightGain }
      .leftLabel { grid-area: leftLabel; text-align: center; }
      .rightLabel { grid-area: rightLabel; text-align: center; }
      
      .showFieldSet { width: 29ch }
      
      .panInput { 
          grid-area: panInput; 
          align-items: center;
          align-self: center;
          text-align: center; 
          width: 100%;
      }
      .panLabel { grid-area: panLabel; text-align: center; }
      
      #visuals {
          width: 100vh;
          position: fixed;
          top: 0;
          left: 0;
          z-index: -10;
          transform-style: preserve-3d;
      }
          
      #PowerShellCode {
          top: 100vh;
          width: 100vw;
      }
      
      pre { text-align: left }
      
      .verticalSlider{ writing-mode: vertical-rl;direction: rtl }
      
      // .colorWheel { filter: url('#colorWheel'); }
      canvas { filter: url('#hueRotate'); }
      #background-svg { filter: url('#hueRotate') }
      .audioControls { text-align: center}
      
      $(
      foreach ($channel in '', '-left','-right') {
          foreach ($propertyName in 
              'lows', 'mids', 'highs',
              'volume', 'frequency',
              'delta'
          ) {
              if ($propertyName -eq 'delta' -and -not $channel) { continue }
      @"
      @property music$channel-$propertyName { syntax: '<number>'; inherits: true; initial-value: 0; }
      "@
          }
      }
      )
      "@
      
      $svgFilters = @'
      <!-- Generated with PSSVG 0.2.10 <https://github.com/StartAutomating/PSSVG> -->
      <svg width='0%' height='0%' xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <filter id="colorWheel">
            <feColorMatrix type="hueRotate">
                <animate attributeName="values" values="0; 360" dur=".42s" repeatCount="indefinite" />
            </feColorMatrix>
            <feMorphology operator="dilate" radius="1" result="dilated">
              <animate attributeName="radius" values="0;1;0" dur="0.42s" repeatCount="indefinite"/>
            </feMorphology>
            <feMorphology operator="erode" radius="1" result="eroded">
              <animate attributeName="radius" values="1;4;1" dur="0.42s" repeatCount="indefinite"/>
            </feMorphology>      
            <feBlend mode="exclusion" in="SourceGraphic" in2="eroded" result="blendedEroded">
              <animate attributeName="mode" values="screen;overlay;screen" dur="0.42s" repeatCount="indefinite"/>
            </feBlend>
            <feBlend mode="exclusion" in2="eroded" in="blendedEroded" />
        </filter>
        <filter id='erodeFilter'>
          <feMorphology in="SourceGraphic" operator="erode" radius="1" result="eroded">
              <animate attributeName="radius" values="0;1;0" dur="4.2s" repeatCount="indefinite"/>
          </feMorphology>
        </filter>
        <filter id='tile' x="0" y="0" width="100%" height="100%">
          <feTile in="SourceGraphic" x="0" y="0" width="100%" height="100%" />
          <feTile />
        </fitler>
        <filter id="emboss">
            <feConvolveMatrix
              kernelMatrix="1 0 0
                            0 0 0
                            0 0 -1" />
        </filter>  
        <filter id='dilateFilter'>
          <feMorphology in="SourceGraphic" operator="dilate" radius="1" result="dilated">
              <!-- <animate attributeName="radius" values="1;2;1" dur="4.2s" repeatCount="indefinite"/> -->
          </feMorphology>
        </filter>  
        <filter id='blurFilter'>
          <feGaussianBlur in="SourceGraphic" stdDeviation="0.5" result="blur" />    
              <animate id='blurFilterAnimation' attributeName="stdDeviation" values="0;2;0" dur=".42s" repeatCount="indefinite"/>
          </feGaussianBlur>
        </filter>
        <filter id='hueRotate'>
          <feColorMatrix in="SourceGraphic" type="hueRotate" values="180">
              <animate attributeName="values" values="0;360" dur="4.2s" repeatCount="indefinite" id='hueRotateAnimation' />
          </feColorMatrix>    
        </filter>
        <filter id='saturate'>
          <feColorMatrix in="SourceGraphic" type="saturate" values="1">
              <!-- <animate attributeName="values" values="0;1;0" dur="4.2s" repeatCount="indefinite"/> -->
          </feColorMatrix>    
        </filter>
        </defs>
      </svg>
      '@
      
      
      $audioPlayer = @"
      <style>
      .audio-player-grid {
          display:grid;
          grid-template-areas:
              ". . . controls . . ."                
              "time last play-pause progress mute-unmute next duration"
              ". . playlist playlist playlist . .";
          place-items: center;
          justify-items: center;
          grid-template-columns: auto auto auto 1fr auto auto auto;
          grid-template-rows: auto auto auto;
          gap: 1rem;
      }
      #audio-player-progress {
          grid-area: progress;
          place-items: center;
          width: 100%;
      }
      #audio-player-range {
          grid-area: progress;
          place-items: center;
          opacity: 0.5;
          width: 100%;
      }
      #lastTrack {
          grid-area: last;
          place-items: center;
      }
      #nextTrack {
          grid-area: next;
          place-items: center;
      }
      #audioFile {
          grid-area: file;
      }
      #audio-player-time {
          grid-area: time;
      }
      #audio-player-duration {
          grid-area: duration;
      }
      .playlist {
          grid-area: playlist;
          place-items: center;
          display: flex;
          flex-direction: row;
          box-sizing: border-box;
          line-height: 2rem;
          gap: 1rem;
      }
      .audio-controls {
          grid-area: controls;
      }
      
      .play-pause {
          grid-area: play-pause;    
      }
      .mute-unmute {
          grid-area: mute-unmute;    
      }
      #audioFile {
          display: none;
      }
      </style>
      
      <div class='audio-player-grid'>
          <div class='audio-controls'>        
              <label for='audioFile'>Select File</label>
              <input type="file" id="audioFile" multiple="true" />    
          </div>
          
      
          <span id='audio-player-time'></span>
      
          <progress id='audio-player-progress' min='0' max='100'></progress>
          
          <input type='range' id='audio-player-range' max='100' min='0' step='1' onchange='
              document.getElementById("audio").currentTime =
                  document.getElementById("audio").duration/100 * event.target.value
          '></input>
          <span id='audio-player-duration'></span>
      
          <section class='play-pause'>
              <button id='playButton' class='invisible'>$(. $site.includes.Feather 'play')</button>
              <button id='pauseButton' class='invisible'>$(. $site.includes.Feather 'pause')</button>
          </section>
      
          <section class='mute-unmute'>
              <button id='volume-unmute' class='invisible'>$(. $site.includes.Feather 'volume-2')</button>
              <button id='volume-mute' class='invisible'>$(. $site.includes.Feather 'volume-x')</button>
          </section>
          
      
          <button id='lastTrack'>$(. $site.includes.Feather 'skip-back')</button>
          <button id='nextTrack'>$(. $site.includes.Feather 'skip-forward')</button>
          <div class='playlist'>
              <label for='select-playlist'>Playlist</label>
              <select id='select-playlist'>        
              </select>
              <button id='removeTrack' class='invisible'>$(. $site.includes.Feather 'file-minus')</button>
          </div>
          <audio autoplay="true" id="audio"></audio>
      </div>
      
      <script type='module'>
          const searchParameters = new URLSearchParams(window.location.search)
          const queryAliases = {
              "Source": ["Source", "source", "Src", "src", "S", "s"]
          }
          const queryParameters = {}
          for (const key of Object.keys(queryAliases)) {            
              for (const p of queryAliases[key]) {
                  if (searchParameters.has(p)) {
                      queryParameters[key] = searchParameters.get(p)
                      break
                  }
              }
          }
          if (queryParameters.Source) {
              try {
                  var preFetch = await fetch(queryParameters.Source).then(r => r.blob())
                  document.getElementById('audio').src = queryParameters.Source
              } catch {
                  console.log('no go')
              }
              
          }
      </script>
      
      <div id='currentlyPlaying'>
          <span id='currentTrackName'>
          </span>
      </div>
      "@
      
      $numberProperties = @()
      $colorProperties = @()
      $checkedProperties = @()
      
      filter toCssVariable {
          ($_ -replace '^-{0,}', '--' -creplace '(?<=\p{Ll})\s{0,}(?=\p{Lu})', '-').ToLower()
      }
      
      filter checkbox {
          $in = $_
          $inVar = $in | toCssVariable
          if ($args -match 'checked') {
              $checkedProperties += $inVar
          } else {
              $numberProperties += $inVar
          }
          
          $inId = $inVar -replace '^--' -replace '-{1,}', '-'   
      
          @(
              "<section>"
              "<label for='$inId'>$([Web.HttpUtility]::HtmlEncode($in))</label>"
              "<input type='checkbox' id='$inId'$(
                  if ($args -match 'checked') { ' checked'}
              )></input>"
              "</section>"
          ) -join [Environment]::NewLine
          
          
      }
      
      filter slider {
          $in = $_
          $inVar = $in | toCssVariable
          $numberProperties += $inVar
          $inId = $inVar -replace '^--' -replace '-{1,}', '-'
          @("<section>"
          "<label for='$inId'>$([Web.HttpUtility]::HtmlEncode($in))</label>"
          "<input type='range' id='$inId'$(
              if ($args) {
                  ' ' + ($args -join ' ')
              }
          )></input>"
          "</section>") -join [Environment]::NewLine
      }
      
      filter verticalSlider {
          $in = $_
          $inVar = $in | toCssVariable
          $numberProperties += $inVar
          $inId = $inVar -replace '^--' -replace '-{1,}', '-'
          "<input type='range' id='$inId'$(
              if ($args) {
                  ' ' + ($args -join ' ')
              }
          )></input>"
          "<label for='$inId'>$([Web.HttpUtility]::HtmlEncode($in))</label>"
      }
      
      filter colorPicker {
          $in = $_
          $inVar = $in | toCssVariable
          $colorProperties += $inVar
          $inId = $inVar -replace '^--' -replace '-{1,}', '-'
          "<input type='color' id='$inId'$(
              if ($args) {
                  ' ' + ($args -join ' ')
              }
          ) onchange='document.body.style.setProperty(`"$inVar`", event.target.value)'></input>"
          "<label for='$inId'>$([Web.HttpUtility]::HtmlEncode($in))</label>"
      }
      
      
      $options = [Ordered]@{
          Audio = @"
      <style>
      .pan-and-gain {
          display: grid;    
          place-items: center; 
          grid-template-areas: 
              'leftGain pan rightGain' 'leftGain rate rightGain';
          gap: 1rem;    
          grid-template-colums: auto max-content auto;
      }
      .rateAndPitch {
          grid-area: rate;
          display: flex;
          flex-direction: column;
          text-align: center;
      }
      .pan {
          text-align: center;
          place-items: center;
          display: flex;
          flex-direction: column;
          grid-area: pan;
      }
      .pan input[type='range'] {
          width: 100%;
      }
      .pan section {
          width: 100%;
      }
      .leftGain {
          grid-area: leftGain;
          display:flex;
          flex-direction: column;
          place-items: center;
      }
      .rightGain {
          grid-area: rightGain;
          display:flex;
          flex-direction: column;
          place-items: center;
      }
      .panInput {width: 100%; }
      </style>
      
      <fieldset class='pan-and-gain'>    
          <section class='leftGain'>
              $('Left Gain' | . verticalSlider "min='0' max='100' value='50' class='verticalSlider'")
          </section>
          <section class='pan'> 
      $(
          'Stereo Pan' | . slider "min='-100' max='100' value='0' class='panInput'"
      )
              <section class='panButtons'>
                  <button onclick='
                      document.getElementById("stereo-pan").value = -100
                      document.body.style.setProperty("--stereo-pan", -100)
                  '>Left</button>
                  <button onclick='
                      document.getElementById("stereo-pan").value = 0
                      document.body.style.setProperty("--stereo-pan", 0)
                  '>Center</button>
                  <button onclick='
                      document.getElementById("stereo-pan").value = 100
                      document.body.style.setProperty("--stereo-pan", 100)
                  '>Right</button>
              </section>
          </section>
          <section class='rightGain'>
              $('Right Gain' | . verticalSlider "min='0' max='100' value='50' class='verticalSlider'")
          </section>    
          <fieldset class='rateAndPitch'>
              <legend>Rate</legend>
              <script>
              function syncPlaybackRate(event) {        
                  document.getElementById('audio').playbackRate = event.target.value
                  document.getElementById('playbackRate').value = event.target.value
                  document.getElementById('playbackRateExact').value = event.target.value
                  event.preventDefault()
              }                            
              </script>
              
              <label for="playbackRate">Playback Rate</label>                            
              <input type='number' id='playbackRateExact' max='8' step='0.005' value='1' maxlength='6' onchange='syncPlaybackRate(event)' />
              <input type='range' id='playbackRate' min='0.1' max='4' step='0.005' value='1' onchange='syncPlaybackRate(event)' />        
              <input type='checkbox' id='keepPitch' checked onchange="document.getElementById('audio').preservesPitch = event.target.checked"/>
              <label for="keepPitch">Keep Pitch</label>
          </fieldset>
          
      </fieldset>
      
      "@    
          Visuals = @"
          <style>
          .show-fill-scopes {
              display: grid;
              grid-template-columns: repeat(10, 1fr);
              grid-template-rows: auto auto;
              place-items: center;
              gap: 1rem;
          }
      
          .channel-scopes {
              display:grid;
              grid-template-columns: repeat(3, 1fr);
              place-items: center;
              gap: 1rem;
          }
          
          .left-scopes {
              display:grid;
              grid-template-columns: 1;
              grid-template-rows: repeat(auto);
              place-items: center;
              gap: 1rem;
          }
          .mono-scopes {
              display:grid;
              grid-template-columns: 1;
              grid-template-rows: repeat(auto);
              place-items: center;
              gap: 1rem;
          }
          .right-scopes {
              display:grid;
              grid-template-columns: 1;
              grid-template-rows: repeat(auto);
              place-items: center;
              gap: 1rem;
          }
      
          </style>
          <fieldset class='PaletteFieldSet'>
              <legend>Palette</legend>                                
              $(if ($site.Includes.SelectPalette) { . $site.Includes.SelectPalette })
              <button id="SetRandomPalette" onclick="SetRandomPalette()">Random Palette</button>
              <input type="checkbox" id="HueRotateSwitch" />
              <label for="HueRotateSwitch">HueRotate</label>
              <input type="number" id="HueRotateMultiplier" value="720" />
              <label for="HueRotateSwitch">x</label>
          </fieldset>    
          <fieldset class='channel-scopes'>
              <legend>Scopes</legend>
              <section class='left-scopes'>
              $(
                  'Show Left Scope' | . checkbox checked
                  'Fill Left Scope' | . checkbox  
                  $(if ($site.includes.SelectColor) {
                      . $site.Includes.SelectColor -id SelectLeftColor -Selected 'brightGreen'
                  })
                  'Show Left Radial' | . checkbox checked
                  'Fill Left Radial' | . checkbox            
                  'Left Radial Frequency' | 
                      slider "min='1' max='8' value='2'"
                  'Left Radial Amplitude' | 
                      slider "min='0' max='100' value='50'"
              )
              </section>
              <section class='mono-scopes'>
              $(
                  'Show Mono Scope' | . checkbox checked
                  'Fill Mono Scope' | . checkbox
                  $(if ($site.includes.SelectColor) { . $site.Includes.SelectColor -Selected 'cyan' })
                  'Show Radial Scope' | . checkbox checked
                  'Fill Radial Scope' | . checkbox
                  'Radial Frequency' | 
                      slider "min='1' max='8' value='2'"
                  'Radial Amplitude' | 
                      slider "min='0' max='100' value='75'"
              )
              </section>
              <section class='right-scopes'>
              $(
                  'Show Right Scope' | . checkbox checked
                  'Fill Right Scope' | . checkbox
                  $(if ($site.includes.SelectColor) {
                      . $site.includes.SelectColor -id SelectRightColor -Selected 'brightRed'
                  })            
                  'Show Right Radial' | . checkbox checked
                  'Fill Right Radial' | . checkbox
                  'Right Radial Frequency' | 
                      slider "min='1' max='8' value='2'"
                  'Right Radial Amplitude' | 
                      slider "min='0' max='100' value='25'"
              )
              </section>
          </fieldset>
          <fieldset>
              <legend>Pattern</legend>
              
              <label for="showPattern">Show Pattern</label>
              <input type="checkbox" id="showPattern" checked="true" />        
              
              <label for="fillPattern">Fill Pattern </label>
              <input type="checkbox" id="fillPattern" />
              
              <label for="evenOddPattern">Even/Odd Fill</label>
              <input type="checkbox" id="evenOddPattern" checked />
              
              
              <label for="SelectPatternColor">Pattern Color</label>
              $(if ($site.includes.SelectColor) {
                  . $site.includes.SelectColor -id SelectPatternColor -Selected 'purple'
              })
          </fieldset>
          <fieldset>
              <legend>Bars</legend>
              <div>
                  <input type="checkbox" id="showBars" checked="true" />
                  <label for="showBars">Bars</label>
                  $(if ($site.includes.SelectColor) {
                      . $site.includes.SelectColor -id SelectBarsColor -Selected 'brightCyan'
                  })
              </div>                            
              <div>
                  <input type="checkbox" id="showVolumeCurve" checked="true" />
                  <label for="showVolumeCurve">Curve</label>
                  $(if ($site.includes.SelectColor) {
                      . $site.includes.SelectColor -id SelectCurveColor -Selected 'brightYellow'
                  })
              </div>
          </fieldset>
          <fieldset>
              <legend>CSS</legend>
              <div>
                  <label for="transformFunction">transform</label>
                  <input id="transform-function" onchange="document.getElementById('visuals').style.transform = event.target.value"></input>
              </div>
              <div>        
                  <label for="background-blend-mode">background-blend-mode</label>
                  <select id='background-blend-mode' onchange="document.body.style['background-blend-mode'] = event.target.value">
                      <option selected>normal</option>
                      <option>darken</option>
                      <option>multiply</option>
                      <option>color-burn</option>
                      <option>lighten</option>
                      <option>screen</option>
                      <option>color-dodge</option>
                      <option>overlay</option>
                      <option>soft-light</option>
                      <option>hard-light</option>
                      <option>difference</option>
                      <option>exclusion</option>  
                      <option>hue</option>
                      <option>saturation</option>
                      <option>color</option>
                      <option>luminosity</option>
                  </select>
              </div>
              <div>
                  <label for="mix-blend-mode">mix-blend-mode</label>
                  <select id='mix-blend-mode' onchange="document.body.style['mix-blend-mode'] = event.target.value">
                      <option>normal</option>
                      <option>darken</option>
                      <option>multiply</option>
                      <option>color-burn</option>
                      <option>lighten</option>
                      <option>screen</option>
                      <option>color-dodge</option>
                      <option>overlay</option>
                      <option>soft-light</option>
                      <option>hard-light</option>
                      <option>difference</option>
                      <option selected>exclusion</option>  
                      <option>hue</option>
                      <option>saturation</option>
                      <option>color</option>
                      <option>luminosity</option>
                  </select>
              </div>        
          </fieldset>
      "@                                
          "View Source" = @"
      <div id='PowerShellCode'>
          <pre>
              <code class='language-PowerShell'>
      $([Web.HttpUtility]::HtmlEncode($MyInvocation.MyCommand.ScriptBlock))
              </code>
          </pre>
      </div>
      "@
          
      }
      
      $options = [Ordered]@{Options=$options}
      
      
      
      filter toTree {
          $in = $_
          $mySelf = $MyInvocation.MyCommand        
          $arguments = @($args)
          if ($in -is [Collections.IDictionary]) {
              # "<ul class='directory'>"
              $in.GetEnumerator() | . $mySelf @arguments
              # "</ul>"        
          } elseif ($in.Key -is [string]) {
              $newArgs = @($arguments) + $in.Key
              "<details class='$($newArgs -join '-')'>"
                  "<summary>$([Web.HttpUtility]::HtmlEncode($in.Key))</summary>"
                  "<ul>"
                  if ($in.Value -is  [Collections.IDictionary]) {                
                      $in.Value | . $mySelf @newArgs
                  } else {
                      $in.Value
                  }
                  "</ul>"
              "</details>"
          }
      }
      
      $cssProperties = @(
      foreach ($checkedProperty in $checkedProperties) {
          "@property $checkedProperty { syntax: '<number>'; inherits: true; initial-value:1}"
      }
      
      foreach ($numberProperty in $numberProperties) {
          "@property $numberProperty { syntax: '<number>'; inherits: true; initial-value:0;}"
      }
      
      foreach ($colorProperty in $colorProperties) {
          "@property $colorProperty { syntax: '<color>'; inherits: true; initial-value:transparent;}"
      }
      )
      
      
      $html = @"
      <style>
      $(
      $cssProperties -join [Environment]::NewLine
      )
      $style
      </style>
      $svgFilters
      <div class='visualsGrid'>
          <canvas id='visuals'></canvas>
      </div>
      <div class='overlay'>
          $audioPlayer
          $($options | toTree)    
      </div>
      
      <script>
      
      var audio = document.getElementById('audio')
      var audioLoader = document.getElementById('audioFile')
      var nextTrack = document.getElementById('nextTrack')
      var lastTrack = document.getElementById('lastTrack')
      var playButton = document.getElementById('playButton')
      var pauseButton = document.getElementById('pauseButton')
      var unmuteButton = document.getElementById('volume-unmute')
      var muteButton = document.getElementById('volume-mute')
      var removeTrack = document.getElementById('removeTrack')
      const selectPlaylist = document.getElementById('select-playlist')
      var playlistFiles = []
      var playlistIndex = 0;
      
      const playlist = {
          index: 0,
          files: [],
          cache: {},
          RemoveTrack: async function() {
              if (! playlist.files.length) { return }
              playlist.files.splice(playlist.index, 1)
              selectPlaylist.remove(playlist.index)
              if (! playlist.files.length) {
                  removeTrack.classList.add('invisible')
                  audio.src = null;
              } else {
                  playlist.NowPlaying()
              }
          },    
          NextTrack: async function() {
              if (! playlist.files.length) { return }
              playlist.index++
              if (playlist.index > playlist.files.length) {
                  playlist.index = 0
              }        
              playlist.NowPlaying()
          },
          LastTrack: async function() {
              if (! playlist.files.length) { return }
              playlist.index--
              if (playlist.index < 0) {
                  playlist.index = playlist.files.length - 1
              }
              playlist.NowPlaying()
          },
          Import: async function(e) {
              for (var i = e.target.files.length - 1 ; i >= 0; i--) {
                  playlist.files.unshift(e.target.files[i])
                  const newOption = document.createElement("option")
                  newOption.value = e.target.files[i].name
                  newOption.text = e.target.files[i].name
                  selectPlaylist.add(newOption, 0)
              }            
              playlist.index = 0        
              playlist.NowPlaying();
      
          },
          NowPlaying: async function() {
              var fileToPlay = playlist.files[playlist.index]
              if (! fileToPlay) { return }
              if (! playlist.cache[fileToPlay.name]) {
                  var reader = new FileReader()
                  reader.readAsDataURL(fileToPlay)
                  reader.onload = async (event) => { 
                      playlist.cache[fileToPlay.name] = event.target.result
                      audio.src = event.target.result
                      // document.getElementById('currentTrackName').innerText = fileToPlay.name
                  }        
              } else {
                  audio.src = playlist.cache[fileToPlay.name]
                  // document.getElementById('currentTrackName').innerText = fileToPlay.name            
              }
                  
              
              if (playlist.files.length > (playlist.index + 1)) {
                  var reader = new FileReader()
                  var nextIndex = playlist.index + 1
                  var nextFile = playlist.files[nextIndex]
                  reader.readAsDataURL(nextFile)
                  reader.onload = async (event) => { 
                      playlist.cache[nextFile.name] = event.target.result
                  }
              }
      
              selectPlaylist.selectedIndex = playlist.index; 
          }
      }
      
      nextTrack.addEventListener('click', (e) => { playlist.NextTrack() })
      lastTrack.addEventListener('click', (e) => { playlist.LastTrack() })
      
      playButton.addEventListener('click', (e) => {     
          audio.play()
      })    
      
      pauseButton.addEventListener('click', (e) => {     
          audio.pause()
      })
      
      muteButton.addEventListener('click', (e) => { 
          muteButton.classList.add('invisible')
          unmuteButton.classList.remove('invisible')        
          audio.muted = true
      })
      
      unmuteButton.addEventListener('click', (e) => { 
          unmuteButton.classList.add('invisible')
          muteButton.classList.remove('invisible')    
          audio.muted = false
      })
      
      removeTrack.addEventListener('click', (e) => {
          playlist.RemoveTrack()
      })
      
      const readers = []
      
      audioLoader.addEventListener('change', playlist.Import, false);
      
      audio.addEventListener('playing', (e) => {
          if (! audioSource) {
              ShowVisualizer();
          }
          playButton.classList.add('invisible')
          pauseButton.classList.remove('invisible')
          if (audio.muted) {
              muteButton.classList.add('invisible')
              unmuteButton.classList.remove('invisible')
          } else {
              unmuteButton.classList.add('invisible')
              muteButton.classList.remove('invisible')        
          }
              
          removeTrack.classList.remove('invisible')
              
          if (document.getElementById('playbackRate')) {
              audio.playbackRate = document.getElementById('playbackRate').value
          }
          if (document.getElementById('keepPitch')) {
              audio.preservePitch = document.getElementById('keepPitch').value
          }
      }, false)
      
      audio.addEventListener('pause', (e)=> {
          playButton.classList.remove('invisible')
          pauseButton.classList.add('invisible')
      })
      
      audio.addEventListener('ended', (e) => {
          if (playlist.index < (playlist.files.length - 1)) {        
              playlist.index++;
              playlist.NowPlaying()        
          }
      }, false)
      
      selectPlaylist.addEventListener('change', (e) => {
          playlist.index = e.target.selectedIndex
          playlist.NowPlaying();
      })
      
      
      // Get a canvas defined with ID "visuals"
      const visualsCanvas = document.getElementById("visuals");
      const visualsCanvas2d = visualsCanvas.getContext("2d");
      const volumeHistory = [];
      const translateDistance = {x:0.0, y:0.0, r: 0.0 };
      const volumeCurves = []
      let frameCount = 0
      let audioSource = null
      
      function isChecked(...ids) {
          for (const id of ids) {        
              if (document.getElementById(id)?.checked) { return true }
          }
          return false    
      }
      
      
      function valueOf(...ids) {        
          for (const id of ids) {
              const element = document.getElementById(id)
              if (element?.value) { return element.value}
          }
          return null    
      }
      
      function valuesOf(...ids) {
          const output = {}
          for (const id of ids) {
              output[id] = document.getElementById(id)?.value
          }
          return output
      }
      
      function propertyValueOf(propertyName) {
          getComputedStyle(visualsCanvas).getPropertyValue(propertyName)
      }
      
      
      async function ShowVisualizer() {    
          const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
          if (! audioSource) {    
              audioSource = audioCtx.createMediaElementSource(document.getElementById("audio"));
          }
          const analyser = audioCtx.createAnalyser();
          analyser.fftSize = 2048;
          const barsAnalyser = audioCtx.createAnalyser();
          barsAnalyser.fftSize = 512;
      
          const bufferLength = analyser.frequencyBinCount;
          const barsBufferLength = barsAnalyser.frequencyBinCount;
          const dataArray = new Uint8Array(bufferLength);
          const frequencyArray = new Uint8Array(barsBufferLength);
      
          const leftFrequencyAnalyser = audioCtx.createAnalyser()
          leftFrequencyAnalyser.fftSize = 2048;
          const leftDataArray = new Uint8Array(bufferLength);
      
          const rightFrequencyAnalyser = audioCtx.createAnalyser()
          rightFrequencyAnalyser.fftSize = 2048;    
          const rightDataArray = new Uint8Array(bufferLength);
      
          const leftBarsAnalyser = audioCtx.createAnalyser();    
          leftBarsAnalyser.fftSize = 512;
          const leftFrequencyArray = new Uint8Array(barsBufferLength);
          
          const rightBarsAnalyser = audioCtx.createAnalyser();    
          rightBarsAnalyser.fftSize = 512;
          const rightFrequencyArray = new Uint8Array(barsBufferLength);
      
          const framesPerSecond = {
              count: 0,
              start: new Date()
          };
          
          
          // For the color bar analyzer we want a average of a few frequencies    
          const colorSelector = document.getElementById('SelectColor')
          const leftColorSelector = document.getElementById('SelectLeftColor')
          const rightColorSelector = document.getElementById('SelectRightColor')
          const colorBarAnalyzer = audioCtx.createAnalyser();
          // so we want use a smaller fftSize
          colorBarAnalyzer.fftSize = 32;
          const colorArray = new Uint8Array(colorBarAnalyzer.frequencyBinCount);        
          const splitter = audioCtx.createChannelSplitter(2);    
          const panner = audioCtx.createStereoPanner()        
          const merger = audioCtx.createChannelMerger(2);
          
          audioSource.connect(panner)
          panner.connect(splitter)
      
          const leftGain = audioCtx.createGain();
          leftGain.gain.setValueAtTime(1, audioCtx.currentTime);
          splitter.connect(leftGain, 0);
      
          const rightGain = audioCtx.createGain()
          rightGain.gain.setValueAtTime(1, audioCtx.currentTime);
          splitter.connect(rightGain, 1)    
      
          // Connect the splitter back to the second input of the merger: we
          // effectively swap the channels, here, reversing the stereo image.
          // leftGain.connect(merger, 0, 1);
          leftGain.connect(leftFrequencyAnalyser)
          leftGain.connect(leftBarsAnalyser)
          leftGain.connect(merger, 0, 0);
          rightGain.connect(rightFrequencyAnalyser)
          rightGain.connect(rightBarsAnalyser)
          rightGain.connect(merger, 0, 1);        
          
          // Connect the source to be analysed
          audioSource.connect(analyser);
          audioSource.connect(barsAnalyser);
      
          merger.connect(audioCtx.destination);
      
      
          function measure(levelsArray, freqArray) {
              let totalVolume = 0.0
              let totalFrequency = 0.0
              let totalLow = 0.0
              let totalMid = 0.0
              let totalHigh = 0.0
              let totalNonZero = 0.0
              let lowCount = 1
              let midCount = 1
              let highCount = 1
              let nonZeroCount = 1
              
              const nonZero = []
              const levels = {
                  all: [],
                  low: [],
                  mid: [],
                  high: [],
                  nonZero: []
              }
              
              const scopeLine = []
              for (let frequencyIndex = 0; frequencyIndex < levelsArray.length; frequencyIndex++) {            
                  const frequencyValue = levelsArray[frequencyIndex];
                  const frequencyRatio = frequencyValue/255.0                        
                  let frequencyDelta = frequencyRatio            
                  levels.all.push(frequencyRatio)
                  if (frequencyValue > 0 ) {
                      
                      levels.nonZero.push(frequencyRatio)
                      totalNonZero += frequencyValue
                      nonZeroCount++
                  }
                  totalVolume += frequencyValue;
                  if (frequencyValue > 0 && frequencyIndex < (levelsArray.length / 3)) {                    
                      // low frequencies                                
                      levels.low.push(frequencyRatio)
                      totalLow += frequencyValue;
                      lowCount++
                  } else if (frequencyValue > 0 && frequencyIndex < (2 * (levelsArray.length / 3))) {
                      // mid frequencies                
                      levels.mid.push(frequencyRatio)
                      totalMid += frequencyValue;
                      midCount++
                  } else if (frequencyValue > 0) {
                      // high frequencies                
                      levels.high.push(frequencyRatio)
                      totalHigh += frequencyValue;
                      highCount++
                  }    
              }
              
              const averageVolume = (totalVolume / levelsArray.length) / 255.0;
              const averageLow = (totalLow / lowCount) / 255.0;
              const averageMid = (totalMid / midCount) / 255.0;
              const averageHigh = (totalHigh / highCount)  / 255.0;
              const averageNonZero = (totalNonZero / nonZeroCount)  / 255.0;
              
              for (let sampleIndex = 0; sampleIndex < freqArray.length; sampleIndex++) {
                  const sampleValue = freqArray[sampleIndex];
                  scopeLine.push(sampleValue/128.0)
                  totalFrequency += sampleValue;
              }
      
              const averageFrequency = (totalFrequency / freqArray.length) / 255.0;
      
              return {
                  average: {
                      volume: averageVolume,
                      frequency: averageFrequency,
                      low: averageLow,
                      mid: averageMid,
                      high: averageHigh,
                      nonZero: averageNonZero
                  },
                  levels: levels,            
                  scope: scopeLine
              }
          }
      
      
          // draw an oscilloscope of the current audio source
          function draw() {
      
              // First, request the next animation frame to call this
              requestAnimationFrame(draw);
      
              framesPerSecond.count++
      
              const audioPlayerElement = document.getElementById('audio');
              const audioPlayerProgress = document.getElementById('audio-player-progress');
              const audioPlayerTime = document.getElementById('audio-player-time')
              const audioPlayerDuration = document.getElementById('audio-player-duration')
      
              if (audioPlayerElement.duration && ! (
                  framesPerSecond.count % 11
              )) {
                  audioPlayerProgress.value = 
                      (audioPlayerElement.currentTime / audioPlayerElement.duration) * 100
                  
                  audioPlayerTime.innerText =
                      Math.floor(audioPlayerElement.currentTime / 60).toString().padStart(2, '0') + ":" +
                          Math.round(audioPlayerElement.currentTime % 60).toString().padStart(2, '0')
                  audioPlayerDuration.innerText =
                      Math.floor(audioPlayerElement.duration / 60).toString().padStart(2, '0') + ":" + 
                          Math.round(audioPlayerElement.duration % 60).toString().padStart(2, '0')
              }
      
              // Then increment our frame count
              frameCount++
      
              const optionNames = ['Bars','Left','Pattern','RadialScope','Right','Scope','Stereo','VolumeCurve']
              const options = {}
              for (const groupName of ['show','fill','evenOdd']) {
                  options[groupName] = {}
                  for (const optionName of optionNames) {
                      options[groupName][optionName.substring(0,1).toLowerCase() + optionName.substring(1)] = isChecked(groupName + optionName)
                  }
              }        
              const show = options.show
              const fill = options.fill
              const evenOdd = options.evenOdd
              const style = document.body.style
      
              const colors = {
                  bars: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectBarsColor')
                  ),                                
                  left: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectLeftColor')
                  ),
                  right: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectRightColor')
                  ),
                  scope: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectScopeColor')
                  ),
                  pattern: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectPatternColor')
                  ),
                  radial: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectRadialColor')
                  ),            
                  curve: getComputedStyle(visualsCanvas).getPropertyValue(
                      valueOf('SelectCurveColor')
                  )
              }
      
              // Then, get our data from the Analyzers
              analyser.getByteTimeDomainData(dataArray);
      
              
              leftFrequencyAnalyser.getByteTimeDomainData(leftDataArray)
              rightFrequencyAnalyser.getByteTimeDomainData(rightDataArray)
              barsAnalyser.getByteFrequencyData(frequencyArray);
              leftBarsAnalyser.getByteTimeDomainData(leftFrequencyArray)
              rightBarsAnalyser.getByteTimeDomainData(rightFrequencyArray)
          
              
              // Adjust the panner
              let pannerValue = document.body.style.getPropertyValue('--stereo-pan')
              if (pannerValue) { panner.pan.value = pannerValue / 100; }        
      
              // Set the channel gains
              let leftGainValue = document.body.style.getPropertyValue('--left-gain')
              if (leftGainValue) { leftGain.gain.value = leftGainValue / 50 }            
              let rightGainValue = document.body.style.getPropertyValue('--right-gain')
              if (rightGainValue) { rightGain.gain.value = rightGainValue / 50 }
              
              // And measure the audio
              const info = measure(frequencyArray, dataArray);    
      
              let leftInfo = null
              let rightInfo = null
              let channelDelta = 0
              let measurements = []
      
              const computedStyle = document.body.style
              if (computedStyle) {
                  computedStyle.setProperty('--volume',info.average.volume)
                  computedStyle.setProperty('--frequency',info.average.frequency)
                  computedStyle.setProperty('--music-lows',info.average.low)
                  computedStyle.setProperty('--music-mids',info.average.middle)
                  computedStyle.setProperty('--music-highs',info.average.high)
              }
                      
              
              leftInfo = measure(leftFrequencyArray, leftDataArray)     
              rightInfo = measure(rightFrequencyArray, rightDataArray)
              channelDelta = leftInfo.average.volume - rightInfo.average.volume
              measurements.push(rightInfo)
              measurements.push(leftInfo)
              if (computedStyle) {
                  computedStyle.setProperty('--music-left-volume',leftInfo.average.volume)
                  computedStyle.setProperty('--music-left-frequency',leftInfo.average.frequency)
                  computedStyle.setProperty('--music-left-lows',leftInfo.average.low)
                  computedStyle.setProperty('--music-left-mids',leftInfo.average.middle)
                  computedStyle.setProperty('--music-left-highs',leftInfo.average.high)
                  computedStyle.setProperty('--music-right-volume',rightInfo.average.volume)
                  computedStyle.setProperty('--rightFrequency',rightInfo.average.frequency)
                  computedStyle.setProperty('--music-right-lows',rightInfo.average.low)
                  computedStyle.setProperty('--music-right-mids',rightInfo.average.middle)
                  computedStyle.setProperty('--music-right-highs',rightInfo.average.high)
      
                  computedStyle.setProperty('--music-left-delta',
                      leftInfo.average.volume - rightInfo.average.volume
                  )
                  computedStyle.setProperty('--music-right-delta',
                      rightInfo.average.volume - leftInfo.average.volume
                  )
              }
          
              measurements.push(info)
              // Most of what we visualize is based off of levels.
              const levels = info.levels;
              let saturateFilter = document.getElementById('saturate')
              // saturateFilter.setAttribute('values', (info.average.low + info.average.high) * 1.25)
      
              let hueRotateFilter = document.getElementById('hueRotateAnimation')
              if (hueRotateFilter) {
                  if (document.getElementById('HueRotateSwitch')?.checked) {
                      let hueRotateMultiplier = document.getElementById('HueRotateMultiplier')?.value 
                      if (! hueRotateMultiplier) { hueRotateMultiplier = 720 }
                      hueRotateFilter.setAttribute('values', 
                          ((info.average.volume + info.average.frequency) * hueRotateMultiplier)
                      )
                  } else {
                      hueRotateFilter.setAttribute('values', 0)
                  }
              }        
      
              let patternColor = getComputedStyle(visualsCanvas).getPropertyValue(leftColorSelector.value)
          
              let leftColor = getComputedStyle(visualsCanvas).getPropertyValue(leftColorSelector.value)
              if (! leftColor) { leftColor = 'green' }
              let rightColor = getComputedStyle(visualsCanvas).getPropertyValue(rightColorSelector.value)
              if (! rightColor) { rightColor = 'red' }
      
              // Get our turtle path and pattern
              let turtlePattern = document.getElementById("turtle-pattern")
              let turtlePath = document.getElementById("turtle-path")
      
              // If we are showing the path / pattern
              if (turtlePattern && show.pattern) {
                  // Let us "wobble" a bit from our center based off of the average volume and frequency
                  translateDistance.x = (info.average.volume * 23) + (info.average.frequency) * 42;
                  translateDistance.y = (info.average.volume * 23) + (info.average.frequency - 0.5) * 42;
                  // and slightly wobble in rotation
                  translateDistance.r = ( (info.average.frequency - 0.5) * 180)
                  // if things are not silent
                  if (info.average.volume > 0) {
                      // Then we want to transform the pattern based off of volume
                      let scaleX = info.average.volume + (info.average.low*1.6)/(info.average.frequency)
                      let scaleY = info.average.volume + (info.average.low*0.4+info.average.mid*0.8+info.average.high*1.5)/(info.average.frequency)
                      turtlePattern.setAttribute("patternTransform", ``
                          translate(`${translateDistance.x} `${translateDistance.y})                    
                          scale(`${scaleX} `${scaleY}`)
                      ``);
                  }
                  
                  if (turtlePath) {
                      turtlePath.setAttribute("opacity", (info.average.volume + info.average.low)/2);
                      turtlePath.style.setProperty('--foreground', colors.pattern)
                      if (fill.pattern) {
                          turtlePath.setAttribute("fill", colors.pattern)
                          if (evenOdd.pattern) {
                              turtlePath.setAttribute("fill-rule", "evenodd")    
                          } else {
                              turtlePath.setAttribute("fill-rule", "nonzero")
                          }                    
                          turtlePath.setAttribute("stroke", "transparent")
                      } else {
                          turtlePath.setAttribute("stroke", colors.pattern)
                          turtlePath.setAttribute("fill", "transparent")
                      }
                      
                  }
              } else if (turtlePattern && ! show.pattern) {
                  let turtlePath = document.getElementById("turtle-path")
                  if (turtlePath) {
                      turtlePath.setAttribute("opacity", 0);
                  }
              }
      
              // We want to change the rotation by setting its animation.
              // Why?  Because it ensures that it will not use the natural rotation animation
              // (this would rotation overload).
              let rotatePattern = document.getElementById("rotate-pattern")
              if (rotatePattern && show.pattern) {
                  rotatePattern.setAttribute('values', (audio.currentTime/60 * 360 * 33) - (info.average.volume * 30) - translateDistance.r)
              }
             
              // Next up is creation of an automatic note color.
              // This area could use some improvement, which is why is not on by default.
              const notePercent = {}
              notePercent['red']   = info.average.low;
              notePercent['green'] = info.average.mid;
              notePercent['blue']  = info.average.high;
              const noteRGB = {}
      
              let baseColor = getComputedStyle(visualsCanvas).getPropertyValue(colorSelector.value);
      
              noteRGB['red'] = Math.floor(Math.min(info.average.volume + (info.average.low * 1.5) * 255, 255));
              noteRGB['green'] = Math.floor(Math.min(info.average.volume + (info.average.mid * 2.1) * 255, 255));
              noteRGB['blue'] = Math.floor(Math.min(info.average.volume + (info.average.high * 1.6) * 255, 255));
              noteRGB['color'] = ``#`${noteRGB.red.toString(16).padStart(2, '0')}`${noteRGB.green.toString(16).padStart(2, '0')}`${noteRGB.blue.toString(16).padStart(2, '0')}``;                    
      
              // getComputedStyle(document).setPropertyValue('--foreground',noteRGB['color'])
      
              // Ok, let us set up our foregroundColor 
              let foregroundColor = ''
              if (document.getElementById('autoColor')?.checked) {
                  // If we wanted to use the auto color,
                  // change it and the value
                  foregroundColor = noteRGB['color']
                  if (turtlePath) {
                      // and change the foreground variable within the path.
                      turtlePath.style.setProperty('--foreground', foregroundColor)
                  }            
              }
              else if (document.getElementById('showCustomColor')?.checked) {
                  // If we wanted to use a custom color, change values accordingly
                  foregroundColor = document.getElementById('customColor').value            
                  if (turtlePath) {                
                      turtlePath.style.setProperty('--foreground', foregroundColor)
                  }
              }
              else {
                  // Otherwise, use the color CSS variable selected in the dropdown.
                  foregroundColor = getComputedStyle(visualsCanvas).getPropertyValue(colorSelector.value)
                  if (turtlePath) {
                      turtlePath.style.setProperty('--foreground', colors.pattern)
                  }
              }
             
              // Make our visuals take up the whole screen
              visualsCanvas.width = window.innerWidth
              visualsCanvas.height = window.innerHeight        
              visualsCanvas.style.width = "100%"
              visualsCanvas.style['margin-left'] = "0%"
              // And set our values accordingly.
              const visualsWidth = window.innerWidth
              const visualsHeight = window.innerHeight
          
              // One would think we would need to clear the rectangle, but one would be wrong.
              // One is not quite sure why this is the case.
              // visualsCanvas2d.clearRect(0, 0, visualsWidth, visualsHeight)
      
              // Our first set of lines are defined by the average bassline
              visualsCanvas2d.lineWidth = info.average.low * 4.2;        
              visualsCanvas2d.strokeStyle = foregroundColor;
              let x = 0;
              let scopes = []
              let nonZeros = [] 
              let channelNames = []           
          
              channelNames.push("right")
              channelNames.push("left")
              scopes.push(rightInfo.scope)
              scopes.push(leftInfo.scope)
              nonZeros.push(rightInfo.levels.nonZero)
              nonZeros.push(leftInfo.levels.nonZero)
          
              channelNames.push("mono")
              scopes.push(info.scope)
              nonZeros.push(info.levels.nonZero)            
              
              // If we are showing a scopes,
              
              // let us draw each scope in a loop
              nextScope: for (let scopeIndex =0; scopeIndex < scopes.length; scopeIndex++) {                
                  const scope = scopes[scopeIndex]
                  const nonZero = nonZeros[scopeIndex]
                  let fillScope = false;
                  // We are going to turn this into an SVG path
                  const scopePath = []
                  // This is actually pretty easy:
                  // Our scope is a range of values between 0 and 2.
                  // This makes most of the math easy.
                  // For a standard ossciloscope, 
                  // we start by dividing the screen into slices
                  let sliceWidth = visualsWidth / scope.length;
                  x = 0
                                  
                  // and go over each point in our scope
                  for (let i = 0; i < scope.length; i++) {                    
                      // our 'vertical' value is translated into the range of `[1,-1]`
                      const v = scope[i] - 1;
                      // we want the scope to max out at 1/3 of the screen size
                      // so we weight our value by that number
                      let weight = (visualsHeight/3)
                      // We determine our point in the nonZero volume array
                      let nonZeroIndex = Math.floor(i/scope.length * nonZero.length)
                      // and multiply the weight
                      weight *= nonZero[nonZeroIndex]
                      // to calculate y, we take half of the height and add our weighted value.                
                      const y = (visualsHeight / 2) + v * weight
                      // we have to start the line at the first point
                      // every other point is a line segment.
                      if (i === 0) { scopePath.push(``M `${x} `${y}``)
                      } else { scopePath.push(``L `${x} `${y}``) }
                      // Increment our x and continue to the next point
                      x += sliceWidth;
                  }
      
                  // Congratulations, we now have a path of our first ossiloscope!            
                  const scopePath2D = new Path2D(scopePath.join(' '))
                  // just set the color
                  if (channelNames[scopeIndex] == "mono") {
                      visualsCanvas2d.strokeStyle = foregroundColor
                      visualsCanvas2d.fillStyle = foregroundColor
                      if (style.getPropertyValue('--show-mono-scope') <= 0 ) continue nextScope                
                      if (style.getPropertyValue('--fill-mono-scope') > 0) {
                          visualsCanvas2d.fill(scopePath2D)
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }
                  if (channelNames[scopeIndex] == "right") {
                      visualsCanvas2d.strokeStyle = rightColor
                      visualsCanvas2d.fillStyle = rightColor
                      if (style.getPropertyValue('--show-right-scope') <= 0 ) continue nextScope
                      
                      if (style.getPropertyValue('--fill-right-scope') > 0) {
                          visualsCanvas2d.fill(scopePath2D)
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }
                  if (channelNames[scopeIndex] == "left") {
                      visualsCanvas2d.strokeStyle = leftColor
                      visualsCanvas2d.fillStyle = leftColor
                      if (style.getPropertyValue('--show-left-scope') <= 0 ) continue nextScope
                      if (style.getPropertyValue('--fill-left-scope') > 0) {
                          visualsCanvas2d.fill(scopePath2D)
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }                            
              }        
              
              
              let radialFrequency = 0
              let radialAmplitude = 0
              nextScope: for (let scopeIndex =0; scopeIndex < measurements.length; scopeIndex++) {                
                  const scope = measurements[scopeIndex].scope            
      
                  if (channelNames[scopeIndex] == "right") {
                      radialFrequency = style.getPropertyValue('--right-radial-frequency')
                      radialAmplitude = style.getPropertyValue('--right-radial-amplitude')
                  } else if (channelNames[scopeIndex] == "left") {              
                      radialFrequency = style.getPropertyValue('--left-radial-frequency') * -1
                      radialAmplitude = style.getPropertyValue('--left-radial-amplitude')
                  } else if (channelNames[scopeIndex] == "mono") {
                      radialFrequency = style.getPropertyValue('--radial-frequency') * -1
                      radialAmplitude = style.getPropertyValue('--radial-amplitude')
                  }
                      
                  // We are going to turn this into an SVG path
                  const scopePath = []
                  const centerX = visualsWidth / 2
                  const centerY = visualsHeight / 2
                  let volumeWeight = info.average.volume
                  const radius = Math.min(centerX, centerY) * (
                      radialAmplitude / 100
                  ) * volumeWeight
                  let angleStep = (Math.PI * 2) / scope.length                
                                  
                  for (let i = 0; i < scope.length; i++) {
                      let angle = angleStep*i
                      if (i == (scope.length - 1)) {
                          scopePath.push('z')
                          continue
                      }
                      const v = scope[i]
                      const x = centerX + Math.cos(
                          angleStep * radialFrequency * i
                      ) * radius * v                    
                      const y = centerY + Math.sin(
                          angleStep * radialFrequency * i
                      ) * radius * v
                      if (angle === 0) {
                          scopePath.push(``M `${x} `${y}``)
                      } else {
                          scopePath.push(``L `${x} `${y}``)
                      }
                  }
                                          
                  // Congratulations, we now have a radial ossiloscope!
                  const scopePath2D = new Path2D(scopePath.join(' '))
                  // just set the color
                  if (channelNames[scopeIndex] == "mono") {
      
                      visualsCanvas2d.strokeStyle = foregroundColor
                      visualsCanvas2d.fillStyle = foregroundColor
                      if (style.getPropertyValue('--show-radial-scope') <= 0) continue nextScope
                      if (style.getPropertyValue('--fill-radial-scope') > 0) {                    
                          visualsCanvas2d.fill(scopePath2D, 'evenodd')
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }
                  else if (channelNames[scopeIndex] == "right") {
                      visualsCanvas2d.strokeStyle = rightColor
                      visualsCanvas2d.fillStyle = rightColor
                      if (style.getPropertyValue('--show-right-radial') <= 0) continue nextScope
                      if (style.getPropertyValue('--fill-right-radial') > 0) {                
                          visualsCanvas2d.fill(scopePath2D, 'evenodd')                
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }
                  else if (channelNames[scopeIndex] == "left") {
                      visualsCanvas2d.strokeStyle = leftColor
                      visualsCanvas2d.fillStyle = leftColor
                      if (style.getPropertyValue('--show-left-radial') <= 0) continue nextScope
                      if (style.getPropertyValue('--fill-left-radial') > 0) {
                          visualsCanvas2d.fill(scopePath2D, 'evenodd')
                      }
                      visualsCanvas2d.stroke(scopePath2D)
                  }                                                
              }                                
              
              if (show.bars || show.volumeCurve) {
                  x = 0;            
                  const gapWidth = 3
                  const barLevels = levels.nonZero
                  let barWidth = ((visualsWidth * 1.0) / barLevels.length) - gapWidth
                  let barHeight = 0;
                  
                  let path = []
                  let barsPath = []
                  for (let i = 0; i < barLevels.length; i++) {
                      barHeight = barLevels[i] * visualsHeight * 1/8;                
                      let rectTop = visualsHeight - barHeight
                      if (show.bars) {
                          visualsCanvas2d.fillStyle = colors.bars;
                          visualsCanvas2d.strokeStyle = colors.curve;
                          visualsCanvas2d.fillRect(x, visualsHeight - barHeight, barWidth, barHeight);
                      }                
      
                      if (i == 0 ) {
                          path.push(``M 0 `${visualsHeight}``)
                          path.push(``L `${x} `${rectTop - info.average.low * 4.2}``)
                      } else {                    
                          path.push(``L `${x + (barWidth + gapWidth ) / 2} `${rectTop - info.average.low * 4.2}``)
                      }
                      x += barWidth + gapWidth;
                  }                            
      
                  if (show.volumeCurve) {
                      let path2d = new Path2D(path.join(' '))
                      //path.strokeWidth = 1
                      
                      if (! show.bars) {
                          visualsCanvas2d.fillStyle = colors.bars
                          visualsCanvas2d.fill(path2d)
                      }
                          
                      visualsCanvas2d.lineWidth = info.average.volume * 4.2;
                      visualsCanvas2d.strokeStyle = colors.curve
                      visualsCanvas2d.stroke(path2d)
                  }            
              }
          }
          
          draw();
      }
      for (const input of [...document.body.querySelectorAll('input')]) {
          console.log('Found Input' + input.id)
          input.addEventListener('change', (e) => {
              console.log('Input changed' + e.target.id)
              if (e.target.id && e.target.value) {
                  
                  if (e.target.type == 'radio' || e.target.type == 'checkbox') {            
                      console.log('Setting property:' + e.target.checked)
                      document.body.style.setProperty('--' + e.target.id, Number(event.target.checked))
                  } else {
                      console.log('Setting property on ' + e.target.type + ' : ' + e.target.value)
                      document.body.style.setProperty('--' + e.target.id, e.target.value)
                  }
                  
              }
          })
          if (input.id) {
              if (input.checked) {
                  document.body.style.setProperty('--' + input.id, Number(input.checked))
              } else if (input.value) {
                  if (input.type == 'color') {
                      document.body.style.setProperty('--' + input.id, input.value)
                  } else if (new Number(input.value)) {
                      document.body.style.setProperty('--' + input.id, Number(input.value)) 
                  }
                  
              }
              
          }
      }
      </script>
      
      "@
      $html
      
      $OnResize