;(function () {
  const id = 'ea40d1a7-f880-4d64-ace1-317beac215b8'
  const sessionId = '4791c95c-bd07-4f02-8ee8-28c127d75f0b'
  const apiUrl = 'https://sharefol.io/api'
  const data = [{"id":"t49","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":null,"duration":139857,"waveform":"https://sharefolio-public.s3.amazonaws.com/enormoushandsomelyToad/af9665b2-a84f-4bab-b6d3-912c59a1095d.dat","metadata":{"album":null,"albumArtist":null,"composer":"Jordan Michael Reed","genre":"Various","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"General demo reel of some recent work!"}},"text":"Demo Reel (2024)","parent":"p5"}]
  const styles = {"bgColor":null,"fgColor":null,"durationText":{"bold":false,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"primaryColor":"#1A7A3E","showWaveform":1,"trackArtSize":"normal","folderArtSize":"normal","letterSpacing":0,"highlightColor":null,"secondaryColor":"#787878","trackAlbumText":{"bold":false,"size":"12px","color":"#cecece","hidden":false,"allCaps":false},"trackTitleText":{"bold":true,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"folderTitleText":{"bold":true,"size":"20px","color":"#fff","hidden":false,"allCaps":false},"metadataArtSize":"normal","trackArtistText":{"bold":false,"size":"12px","color":"#cecece","hidden":true,"allCaps":false},"trackBackground":null,"showPlayControls":1,"folderBorderColor":"#aaaaaa","metadataLabelText":{"bold":true,"size":"14px","color":"#cecece","hidden":false,"allCaps":false},"metadataValueText":{"bold":false,"size":"16px","color":"#fff","hidden":false,"allCaps":false},"trackDurationText":{"bold":false,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"folderChevronColor":"#ffffff","currentlyPlayingText":{"bold":true,"size":"16px","color":"#fff","hidden":false,"allCaps":false},"showTrackInformation":1,"showTrackProgressBar":0,"trackBackgroundHover":"#111114","trackPlayButtonColor":"#fff","folderDescriptionText":{"bold":false,"size":"12px","color":"#dedede","hidden":false,"allCaps":false}}
  const isPreview = false
  function init() {
    let selector
    if (!isNaN(Number(id.slice(0, 1)))) {
      selector = `#\\3${id.slice(0, 1)} ${id.slice(1)}`
    } else {
      selector = `#${id}`
    }
    const divs = [...document.querySelectorAll(selector)].filter(
      (element) => !element.dataset.bound,
    )
    let div = divs[0]
    if (!div) {
      div = document.createElement('div') // Create a new div
      let script = document.scripts[document.scripts.length - 1] // A reference to the currently running script
      script.parentElement.insertBefore(div, script) // Add the newly-created div to the page
    }
    new Player(id, data, div, styles)
  }
  class Player {
    constructor(id, data, container, styles) {
      this.id = id
      this.data = data
      this.container = container
      this.styles = {
        trackArtSize: 'normal',
        showWaveform: true,
        showPlayControls: true,
        showTrackInformation: true,
        showTrackProgressBar: false,
        fontFamily: 'Open Sans',
        letterSpacing: 0,
        fgColor: null,
        bgColor: null,
        highlightColor: null,
        primaryColor: '#d63b5c',
        secondaryColor: '#787878',
        trackBackground: null,
        trackBackgroundHover: '#111114',
        trackPlayButtonColor: '#fff',
        currentlyPlayingText: {
          size: '16px',
          color: '#fff',
          bold: true,
          hidden: false,
          allCaps: false,
        },
        durationText: {
          size: '14px',
          color: '#fff',
          bold: false,
          hidden: false,
          allCaps: false,
        },
        trackTitleText: {
          size: '14px',
          color: '#fff',
          bold: true,
          hidden: false,
          allCaps: false,
        },
        trackArtistText: {
          size: '12px',
          color: '#cecece',
          bold: false,
          hidden: true,
          allCaps: false,
        },
        trackAlbumText: {
          size: '12px',
          color: '#cecece',
          bold: false,
          hidden: false,
          allCaps: false,
        },
        trackDurationText: {
          size: '14px',
          color: '#fff',
          bold: false,
          hidden: false,
          allCaps: false,
        },
        metadataLabelText: {
          size: '14px',
          color: '#cecece',
          bold: true,
          hidden: false,
          allCaps: false,
        },
        metadataValueText: {
          size: '16px',
          color: '#fff',
          bold: false,
          hidden: false,
          allCaps: false,
        },
        metadataArtSize: 'normal',
        folderTitleText: {
          size: '20px',
          color: '#fff',
          bold: true,
          hidden: false,
          allCaps: false,
        },
        folderDescriptionText: {
          size: '12px',
          color: '#dedede',
          bold: false,
          hidden: false,
          allCaps: false,
        },
        folderBorderColor: '#aaaaaa',
        folderChevronColor: '#ffffff',
        folderArtSize: 'normal',
        ...styles,
      }
      this.uuid = 'mp' + id.split('-').pop()
      this.state = 'paused'
      this.audioController = 'element'
      this.tracks = {}
      this.trackOrder = []
      this.groups = {}
      this.openGroups = []
      this.uiDirty = false
      this.audioContext = undefined
      this.audioAnalyzer = undefined
      this.analyzer = undefined
      this.audioSource = undefined
      this.source = undefined
      this.gainNode = undefined
      this.audioGainNode = undefined
      this.startTime = 0
      this.ellapsedTime = 0
      this.pausedTime = 0
      this.mousePosition = 0
      this.selectedTrack = null
      this.defaultTrack = true
      this.loading = false
      this.showMetadata = false
      this.waveData = {}
      this.loadedTracks = {}
      this.waveCanvas = null
      this.loopState = localStorage.getItem('mp-loopState') || 'default' // default | infinite | disabled
      this.loop = {
        time: undefined,
        loopTo: undefined,
        count: 0,
      }
      this.tickMS = 27
      this.crossfadeState = 'pending'
      this.crossfadeAmount = 0
      this.songLookaheadTrigger = true
      this.volume = localStorage.getItem('mp-volume') || 0.75
      this.defaultTheme = {
        bgColor: '#666',
        fgColor: '#5e263a',
        highlightColor: '#e44967',
        visualizer: 'default',
      }
      container.dataset.bound = 1
      this.init()
      document.dispatchEvent(
        new CustomEvent('mpOnReady', { detail: { id: this.id, container } }),
      )
    }
    init() {
      if (this.isVisible()) {
        this.insertIntoDom()
        this.audioElement = this.getElements('Audio')[0]
        this.waveformElement = this.getElements('WaveformCanvas')[0]
        this.audioElement.preservesPitch = false
        this.audioElement.mozPreservesPitch = false
        this.audioElement.webkitPreservesPitch = false
        this.bindEventHandlers()
        this.selectDefaultTrack()
        this.onVolumeUpdate()
        this.padGroups()
        this.particleSystem = null
        ;(() => {
          this.tick()
        })()
      } else {
        setTimeout(() => {
          this.init()
        }, 250)
      }
    }
    bindEventHandlers() {
      const tracks = [...this.getElements('TrackNode')]
      tracks.forEach((track) => {
        track.addEventListener('click', () =>
          this.onTrackClick(track.dataset.id),
        )
      })
      const groups = [...this.getElements('GroupToggle')]
      groups.forEach((group) => {
        group.addEventListener('click', () =>
          this.onGroupClick(group.dataset.id),
        )
      })
      this.audioElement.addEventListener('loadeddata', (evt) => {
        this.selectedTrack.data.loading = false
        this.startParticleSystem()
        this.uiDirty = true
      })
      this.getElements('PlayButton').forEach((element) =>
        element.addEventListener('click', (evt) => this.onPlayClick(evt)),
      )
      this.getElements('PrevButton').forEach((element) =>
        element.addEventListener('click', () => {
          const prevTrack = this.getPrevTrack(this.selectedTrack)
          this.playTrack(prevTrack.id, 0)
        }),
      )
      this.getElements('NextButton').forEach((element) =>
        element.addEventListener('click', () => {
          const nextTrack = this.getNextTrack(this.selectedTrack)
          this.playTrack(nextTrack.id, 0)
        }),
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'mousedown',
        (evt) => {
          this.onCanvasMouseDown(evt)
        },
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'touchstart',
        (evt) => {
          this.onCanvasMouseDown(evt)
        },
      )
      this.getElements('WaveformCanvas')[0].addEventListener('mouseup', (evt) =>
        this.onCanvasMouseUp(evt),
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'touchend',
        (evt) => this.onCanvasMouseUp(evt),
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'mouseleave',
        (evt) => this.onCanvasMouseLeave(evt.offsetX),
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'touchcancel',
        (evt) => this.onCanvasMouseLeave(),
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'mousemove',
        (evt) => {
          this.mousePosition = evt.offsetX
        },
      )
      this.getElements('WaveformCanvas')[0].addEventListener(
        'touchmove',
        (evt) => {
          this.mousePosition =
            evt.touches[0].clientX - evt.target.getBoundingClientRect().left
        },
      )
      this.audioElement.addEventListener('ended', () => {
        this.onTrackEnded()
      })
      const silence = this.getElements('Silence')[0]
      silence.addEventListener('ended', () => {
        silence.play()
      })
      this.getElements('LoopButton').forEach((element) =>
        element.addEventListener('click', () => {
          this.onLoopButtonClick()
        }),
      )
      this.getElements('MetadataToggle').forEach((element) =>
        element.addEventListener('click', () => {
          this.showMetadata = !this.showMetadata
          this.uiDirty = true
        }),
      )
      this.getElements('VolumeRange')[0].addEventListener('input', (evt) => {
        this.volume = evt.target.value
        if (this.audioContext) {
          let audioVolume = this.volume
          let gainVolume = this.volume
          const currentTime = this.audioContext.currentTime
          if (this.crossfadeAmount > 0) {
            audioVolume = Math.max(0, 1 - this.crossfadeAmount) * this.volume
            gainVolume = this.crossfadeAmount * this.volume
          }
          if (this.isSafari()) {
            this.audioElement.volume = audioVolume
          }
          if (this.audioGainNode !== undefined) {
            this.audioGainNode.gain.linearRampToValueAtTime(
              audioVolume,
              currentTime + 0.025,
            )
          }
          if (this.gainNode !== undefined) {
            this.gainNode.gain.linearRampToValueAtTime(
              gainVolume,
              currentTime + 0.025,
            )
          }
        }
        this.uiDirty = true
      })
      window.addEventListener('keydown', (evt) => {
        const tagName = evt?.target?.tagName
        const badTags = ['BUTTON', 'INPUT', 'TEXTAREA']
        if (evt.keyCode === 32 && !badTags.includes(tagName)) {
          this.onPlayClick()
        }
      })
      document.addEventListener('mpOnPlay', (evt) => {
        const { detail } = evt || {}
        if (detail.id !== this.id && this.isPlaying()) {
          this.pausePlayback()
        }
      })
    }
    onTrackEnded() {
      if (!this.songLookaheadTrigger || document.visibilityState == 'hidden') {
        // Song actually ended, wasn't paused
        const nextTrack = this.getNextTrack(this.selectedTrack)
        this.playTrack(nextTrack.id, 0)
      }
    }
    onVolumeUpdate() {
      const volume = this.volume
      const color = this.styles.secondaryColor
      const element = this.getElements('VolumeButton')[0]
      const volumeBar = this.getElements('VolumeBar')[0]
      if (volume === 0) {
        element.innerHTML = this.volumeMute(color)
      } else if (volume < 0.5) {
        element.innerHTML = this.volumeOne(color)
      } else {
        element.innerHTML = this.volumeTwo(color)
      }
      volumeBar.style.width = `${volume * 120}px`
      localStorage.setItem('mp-volume', volume)
    }
    onGroupClick(id) {
      this.groups[id].data.open = !this.groups[id].data.open
      this.uiDirty = true
    }
    onLoopButtonClick() {
      if (this.loopState === 'default') {
        this.loopState = 'infinite'
      } else if (this.loopState === 'infinite') {
        this.loopState = 'disabled'
      } else if (this.loopState === 'disabled') {
        this.loopState = 'default'
      }
      localStorage.setItem('mp-loopState', this.loopState)
      this.uiDirty = true
    }
    renderDuration(current, total) {
      const element = this.getElements('DurationContainer')[0]
      if (!Number.isNaN(total)) {
        element.innerHTML = `${renderAsMinutes(current)} / ${renderAsMinutes(
          total,
        )}`
        function renderAsMinutes(seconds) {
          const mins = Math.floor(seconds / 60)
          const remaining = Math.floor(seconds - mins * 60)
          return `${mins > 9 ? mins : '0' + mins}:${
            remaining > 9 ? remaining : '0' + remaining
          }`
        }
      }
    }
    renderProgressBar(current, total) {
      const bars = this.getElements('TrackProgressBarAmount')
      const amount = !Number.isNaN(total) ? (current * 100) / total : 0
      const currentTrackId = this.selectedTrack?.id
      bars.forEach((bar) => {
        const trackId = bar.dataset.track
        if (trackId === currentTrackId) {
          bar.style.width = `${amount}%`
        } else {
          bar.style.width = '0%'
        }
      })
    }
    onPlayClick() {
      if (this.isPlaying()) {
        this.pausePlayback()
      } else {
        this.playTrack(this.selectedTrack.id, this.pausedTime)
      }
    }
    pausePlayback() {
      if (this.isPlaying()) {
        this.pausedTime = this.getCurrentTime()
        this.audioContext.suspend()
        if (this.audioController === 'element') {
          this.audioElement.pause()
        }
        this.state = 'paused'
        this.stopParticleSystem()
        this.uiDirty = true
      }
    }
    onCanvasMouseDown() {
      this.canvasMouseDown = true
    }
    onCanvasMouseLeave(offsetX) {
      this.mousePosition = 0
      if (this.canvasMouseDown && offsetX < 0) {
        this.onCanvasMouseUp()
      } else if (
        this.canvasMouseDown &&
        offsetX > this.waveformElement.clientWidth - 10
      ) {
        const nextTrack = this.getNextTrack(this.selectedTrack)
        this.playTrack(nextTrack.id, 0)
      }
      window.getSelection().removeAllRanges()
      this.canvasMouseDown = false
    }
    onCanvasMouseUp() {
      if (this.canvasMouseDown) {
        this.canvasMouseDown = false
        const element = this.waveformElement
        const canvasWidth = element.clientWidth
        const offsetX = this.mousePosition
        if (offsetX > canvasWidth) {
          const nextTrack = this.getNextTrack(this.selectedTrack)
          this.playTrack(nextTrack.id, 0)
        } else {
          const duration = this.getDuration()
          const position = Math.min(
            Math.max(0, (offsetX / canvasWidth) * duration),
            duration,
          )
          if (this.audioController === 'element') {
            this.audioElement.currentTime = position
          } else {
            if (this.isPlaying()) {
              const loopEnd = this.getLoopEnd()
              if (loopEnd < position) {
                this.loop.count = 0
              }
              const source = this.createSource({
                buffer: this.selectedTrack.data.buffer,
                loop: this.loop,
              })
              source.start(0, position)
              this.resetVolumes()
              this.startTime = this.audioContext.currentTime
            }
          }
          this.ellapsedTime = position
          this.pausedTime = position
        }
      }
      this.mousePosition = 0
    }
    async onTrackClick(id) {
      if (this.isPlaying() && this.selectedTrack.id === id) {
        this.pausePlayback()
      } else {
        if (this.selectedTrack.id === id) {
          this.playTrack(id, this.pausedTime)
        } else {
          this.playTrack(id, 0)
        }
      }
    }
    async playTrack(id, offset) {
      if (isPreview) {
        return this.previewTrack(id)
      }
      if (!this.isSafari()) {
        const silence = this.getElements('Silence')[0]
        silence.play()
      }
      document.dispatchEvent(
        new CustomEvent('mpOnPlay', { detail: { id: this.id } }),
      )
      const currentTime = this.getCurrentTime()
      const isNewTrack = id !== this.selectedTrack.id || this.defaultTrack
      this.bindAudioContext()
      this.state = 'playing'
      this.defaultTrack = false
      this.crossfadeAmount = 0
      if (this.source) {
        this.source.stop()
        this.source.disconnect()
      }
      this.audioElement.pause()
      this.resetVolumes()
      //reset gain nodes, etc
      this.uiDirty = true
      this.songLookaheadTrigger = true
      if (isNewTrack) {
        this.selectedTrack = this.tracks[id]
        const loop = this.selectedTrack?.data?.loop || {}
        this.loop = { ...loop }
        this.loadWaveform(id, false)
        this.addParticleSystem(this.selectedTrack)
      }
      this.startTime = this.audioContext.currentTime
      this.ellapsedTime = offset
      if (this.tracks[id]?.data?.buffer) {
        // Track has already been downloaded
        this.audioController = 'api'
        const source = this.createSource({
          buffer: this.tracks[id].data.buffer,
          loop: this.loop,
        })
        if (isNewTrack) {
          source.start()
        } else {
          source.start(0, currentTime)
        }
        this.onNewAudioAnalyzer(this.analyzer)
        this.selectedTrack.data.loading = false
        this.startParticleSystem()
      } else {
        // Start audio element, download buffer
        this.audioController = 'element'
        this.selectedTrack.data.loading = true
        if (isNewTrack) {
          const trackUrlData = await this.getTrackUrl(id)
          this.audioElement.src = trackUrlData.url
          if (!this.isSafari()) {
            setTimeout(() => this.getTrackBuffer(id, trackUrlData.url), 1000)
          }
        }
        if (this.isSafari()) {
          this.audioElement.volume = this.volume
        }
        this.audioElement.play()
        this.audioElement.currentTime = offset
        this.onNewAudioAnalyzer(this.audioAnalyzer)
      }
      this.audioContext.resume()
    }
    resetVolumes() {
      const cancelTime = Math.max(0, this.audioContext.currentTime - 10)
      const volume = this.volume
      if (this.gainNode) {
        this.gainNode.gain.cancelScheduledValues(cancelTime)
        this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime)
      }
      if (this.audioGainNode) {
        this.audioGainNode.gain.cancelScheduledValues(cancelTime)
        this.audioGainNode.gain.setValueAtTime(
          volume,
          this.audioContext.currentTime,
        )
      }
    }
    async previewTrack(id) {
      this.selectedTrack = this.tracks[id]
      const loop = this.selectedTrack?.data?.loop || {}
      this.loop = { ...loop }
      this.loadWaveform(id, true)
      this.addParticleSystem(this.selectedTrack)
      this.uiDirty = true
    }
    getTrackBuffer(id, url) {
      if (!this.tracks[id].data.bufferRequested) {
        const request = new XMLHttpRequest()
        request.open('GET', url, true)
        request.responseType = 'arraybuffer'
        this.tracks[id].data.bufferRequested = true
        request.onload = () => {
          let audioData = request.response
          this.audioContext.decodeAudioData(
            audioData,
            (buffer) => {
              this.tracks[id].data.buffer = buffer
              if (this.selectedTrack.id === id && this.state === 'playing') {
                this.gainNode.gain.value = 0
                const source = this.createSource({ buffer, loop: this.loop })
                source.start(0, this.getStartOffset())
                this.crossfadeState = 'active'
              }
            },
            (e) => {
              console.error(`Error with decoding audio data ${e.error}`)
            },
          )
        }
        request.send()
      }
    }
    addParticleSystem(track) {
      const particleSystem = track.data.theme?.particleSystem?.data
      if (particleSystem) {
        this.particleSystem = createParticleSystem({
          background: this.getElements('ParticleBackground')[0],
          foreground: this.getElements('ParticleForeground')[0],
          data: particleSystem,
          player: this,
          waveData: this.waveData[track.id],
        })
      } else {
        this.stopParticleSystem()
        this.particleSystem = null
      }
    }
    startParticleSystem() {
      if (this.particleSystem) {
        this.particleSystem.start()
      }
    }
    stopParticleSystem() {
      if (this.particleSystem) {
        this.particleSystem.stop()
      }
    }
    async getTrackUrl(id) {
      if (!this.loadedTracks[id] || this.isTrackUrlExpired(id)) {
        const playResponse = await fetch(
          `${apiUrl}/player/${this.id}/${id}/play?sessionId=${sessionId}`,
        )
        if (playResponse.ok) {
          const playData = await playResponse.json()
          this.loadedTracks[id] = {
            url: playData.url,
            expires: Date.now() + playData.expires * 1000,
          }
        }
      }
      return this.loadedTracks[id]
    }
    async loadWaveform(id, defaultTrack) {
      if (!this.waveData[id]) {
        this.setLoading(true)
        const url = this.tracks[id]?.data?.waveform
        if (url) {
          const waveformResponse = await fetch(url)
          if (waveformResponse) {
            const buffer = await waveformResponse.arrayBuffer()
            const waveformData = getWaveformData(buffer)
            const waveData = waveformData.data
            const tempData = [...waveData]
              .map((d) => Math.abs(d))
              .sort((a, b) => b - a)
            const max =
              tempData.slice(0, 100).reduce((acc, cur) => acc + cur, 0) / 100
            const min =
              tempData
                .slice(tempData.length - 100, tempData.length)
                .reduce((acc, cur) => acc + cur, 0) / 100
            const normalized = []
            for (let i = 0; i < waveData.length; i += 2) {
              const x = waveData[i]
              const y = waveData[i + 1]
              normalized.push([x / max, y / max])
            }
            const average =
              normalized.reduce(
                (acc, cur) => acc + (Math.abs(cur[0]) + Math.abs(cur[1])) / 2,
                0,
              ) / normalized.length
            const sumSquares = Math.sqrt(
              normalized.reduce((acc, cur) => {
                const amp = Math.max(Math.abs(cur[0]), Math.abs(cur[1]))
                return acc + amp * amp
              }, 0) / normalized.length,
            )
            let drawableData = normalized
            while (drawableData.length > 2500) {
              drawableData = this.reduceWaveData(drawableData)
            }
            const drawableMin = Math.abs(
              Math.min(...drawableData.map((d) => d[0])),
            )
            const drawableMax = Math.max(...drawableData.map((d) => d[1]))
            drawableData = drawableData.map((d) => [
              d[0] / drawableMin,
              d[1] / drawableMax,
            ])
            this.waveData[id] = {
              data: normalized,
              original: waveData,
              min: min / max,
              max,
              average,
              drawableData,
              sumSquares,
            }
          }
        } else {
          const waveformResponse = await fetch(
            `${apiUrl}/player/${this.id}/${id}/waveform`,
          )
          if (waveformResponse.ok) {
            const waveformData = await waveformResponse.json()
            this.waveData[id] = {
              data: waveformData,
            }
          } else {
            this.waveData[id] = {
              error: true,
            }
          }
        }
      }
      const canvas = this.getElements('WaveformCanvas')[0]
      if (this.waveCanvas) {
        this.waveCanvas.state = 'complete'
      }
      this.waveCanvas = new WaveCanvas(
        canvas,
        this,
        this.waveData[id].drawableData,
        {
          ...this.defaultTheme,
          ...(this.selectedTrack?.data?.theme || {}),
        },
        defaultTrack,
      )
      this.onNewAudioAnalyzer(this.audioAnalyzer)
    }
    padGroups() {
      const groups = this.getElements('GroupChildren')
      const padAmount = 8
      const padGroup = (group) => {
        const tracks = this.getChildElements(group, 'TrackNode')
        const childGroups = this.getChildElements(group, 'GroupAccordion')
        const groupsToProcess = this.getChildElements(group, 'GroupNode')
        tracks.forEach((track) => {
          const currentPad = parseInt(track.style.marginLeft, 10) || 0
          const amount = `${currentPad + padAmount}px`
          track.style.marginLeft = amount
          track.style.width = `calc(100% - ${amount})`
        })
        childGroups.forEach((childGroup) => {
          const currentPad = parseInt(childGroup.style.marginLeft, 10) || 0
          const amount = `${currentPad + padAmount}px`
          childGroup.style.marginLeft = amount
          childGroup.style.width = `calc(100% - ${amount})`
        })
        groupsToProcess.forEach(padGroup)
      }
      groups.forEach(padGroup)
    }
    reduceWaveData(data) {
      const reduced = []
      for (let i = 0; i < data.length; i += 2) {
        if (data[i + 1]) {
          const x = (Number(data[i][0]) + Number(data[i + 1][0])) / 2
          const y = (Number(data[i][1]) + Number(data[i + 1][1])) / 2
          reduced.push([x, y])
        } else {
          reduced.push([...data[i]])
        }
      }
      return reduced
    }
    getElements(className) {
      return [
        ...this.container.getElementsByClassName(`${this.uuid}-${className}`),
      ]
    }
    getChildElements(container, className) {
      return [...container.getElementsByClassName(`${this.uuid}-${className}`)]
    }
    updatePlayPause() {
      const color = this.selectedTrack?.data?.theme?.highlightColor || '#e44967'
      const elements = this.getElements('PlayButton')
      elements.forEach((element) => {
        if (this.isPlaying()) {
          if (this.selectedTrack?.data?.loading) {
            element.innerHTML = this.loadingSpinner()
          } else {
            element.innerHTML = this.pauseButton(color, 50, 50)
          }
        } else {
          element.innerHTML = this.playButton(color, 50, 50)
        }
      })
    }
    isTrackUrlExpired(id) {
      return Date.now() > this?.loadedTracks[id]?.expires
    }
    getNextTrack(currentTrack) {
      const index = this.trackOrder.findIndex(
        (child) => child.id === currentTrack.id,
      )
      if (this.trackOrder[index + 1]) {
        return this.trackOrder[index + 1]
      }
      return this.trackOrder[0]
    }
    getPrevTrack(currentTrack) {
      const index = this.trackOrder.findIndex(
        (child) => child.id === currentTrack.id,
      )
      if (this.trackOrder[index - 1]) {
        return this.trackOrder[index - 1]
      }
      return this.trackOrder[this.trackOrder.length - 1]
    }
    getStartOffset() {
      return (
        (this.audioElement.currentTime * 1000 +
          27 +
          this.audioContext.baseLatency * 2) /
        1000
      )
    }
    selectDefaultTrack() {
      let trackId = null
      Object.values(this.tracks).forEach((track) => {
        if (track?.data?.selected) {
          trackId = track.id
        }
      })
      if (!trackId) {
        trackId = Object.keys(this.tracks)?.[0]
      }
      if (trackId) {
        this.selectedTrack = this.tracks[trackId]
        this.loadWaveform(trackId, true)
        this.openParent(this.selectedTrack.parent)
        this.uiDirty = true
      }
    }
    openParent(parentId) {
      const group = this.groups[parentId]
      if (group?.data) {
        group.data.open = true
      }
      if (group?.parent) {
        this.openParent(group.parent)
      }
    }
    getCurrentTime() {
      if (this.audioController === 'element') {
        return this.audioElement.currentTime
      } else if (this.audioController === 'api') {
        const currentTime = this.audioContext.currentTime
        return this.ellapsedTime + currentTime - this.startTime
      }
      return 0
    }
    getDuration() {
      const duration = this?.selectedTrack?.data?.duration || 0
      return duration / 1000
    }
    isPlaying() {
      return this.state === 'playing'
    }
    isVisible() {
      const el = this.container
      return Boolean(
        el && (el.offsetParent || el.offsetWidth || el.offsetHeight),
      )
    }
    tick() {
      if (!this.isVisible()) {
        //this.pausePlayback()
      }
      this.handleUiUpdate()
      this.handleAudioBufferTransition()
      // Handles removing loop if song should end, this way onended will fire
      if (this.songLookaheadTrigger) {
        const currentTime = this.getCurrentTime()
        const duration = this.getDuration()
        if (currentTime + 2 > duration) {
          if (
            this.loop.count === 0 &&
            this.loop.fadeOut &&
            this.loopState !== 'infinite'
          ) {
            this.loop.fadeOut = false
            this.gainNode.gain.setValueAtTime(
              this.gainNode.gain.value,
              this.audioContext.currentTime,
            )
            this.gainNode.gain.linearRampToValueAtTime(
              0,
              this.audioContext.currentTime + duration - currentTime,
            )
          }
        }
        const end = this.source?.loop ? this.source.loopEnd : duration
        if (currentTime + 0.2 > end) {
          // If looping, check if inf, if not subtract and remove loop if required.
          if (
            this.loopState === 'infinite' ||
            (this.loop?.count > 0 && this.loopState === 'default')
          ) {
            this.ellapsedTime = this.getLoopStart(this.loop)
            this.startTime = this.audioContext.currentTime
            if (this.loopState === 'default') {
              this.loop.count -= 1
            }
          } else {
            this.songLookaheadTrigger = false
            if (this.source) {
              this.source.loop = false
            }
          }
        }
      }
      setTimeout(() => {
        this.tick()
      }, this.tickMS)
    }
    getLoopStart(loop) {
      return Number(loop.loopTo)
    }
    getLoopEnd() {
      return this.loop?.loopPointType === 'custom'
        ? Number(this.loop.loopFrom)
        : this.getDuration()
    }
    handleUiUpdate() {
      const currentTime = this.getCurrentTime()
      const duration = this.getDuration()
      let currentDuration = currentTime
      if (this.canvasMouseDown) {
        const canvasWidth = this.waveformElement.clientWidth
        const offsetX = this.mousePosition
        currentDuration = Math.min(
          Math.max(0, (offsetX / canvasWidth) * duration),
          duration,
        )
      }
      this.renderDuration(currentDuration, duration)
      this.renderProgressBar(currentDuration, duration)
      const container = this.getElements('PlayerContainer')[0]
      if (container.clientWidth < 600) {
        container.classList.remove('desktop')
      } else {
        container.classList.add('desktop')
      }
      //ui-dirty
      if (this.uiDirty) {
        this.updateTracks()
        this.updateGroups()
        this.generateSelectedTrackStyle()
        this.onVolumeUpdate()
        this.updatePlayPause()
        this.updateLoopButton()
        this.updatePrevNextButtons()
        this.updateMetadataToggle()
        this.renderMetadata(this.showMetadata, this.selectedTrack)
        this.uiDirty = false
      }
    }
    updatePrevNextButtons() {
      const color = this.selectedTrack?.data?.theme?.highlightColor || '#e44967'
      this.getElements('PrevButton').forEach(
        (element) => (element.innerHTML = this.prevIcon(color)),
      )
      this.getElements('NextButton').forEach(
        (element) => (element.innerHTML = this.nextIcon(color)),
      )
    }
    updateGroups() {
      const groupElements = [...this.getElements('GroupNode')]
      const className = `${this.uuid}-Open`
      groupElements.forEach((groupElement) => {
        const id = groupElement.dataset.id
        if (this.groups[id].data.open) {
          groupElement.classList.add(className)
        } else {
          groupElement.classList.remove(className)
        }
      })
    }
    updateTracks() {
      const title = this.selectedTrack.text
      this.getElements('NameContainer')[0].innerHTML = `${title}`
      const trackElements = [...this.getElements('TrackNode')]
      const selectedClassName = `${this.uuid}-Selected`
      const playingClassName = `${this.uuid}-Playing`
      trackElements.forEach((trackElement) => {
        if (trackElement.dataset.id === this.selectedTrack.id) {
          trackElement.classList.add(selectedClassName)
          if (this.isPlaying()) {
            trackElement.classList.add(playingClassName)
          } else {
            trackElement.classList.remove(playingClassName)
          }
        } else {
          trackElement.classList.remove(selectedClassName, playingClassName)
        }
      })
    }
    onNewAudioAnalyzer(analyzer) {
      if (this.waveCanvas) {
        this.waveCanvas.setAudioAnalyzer(analyzer)
      }
    }
    handleAudioBufferTransition() {
      const { currentTime } = this?.audioContext || {}
      if (this.crossfadeState === 'active') {
        const delta = 0.5
        this.crossfadeAmount = Math.min(1, this.crossfadeAmount + delta)
        if (this.crossfadeAmount >= 1) {
          this.crossfadeState = 'pending'
          this.ellapsedTime = this.audioElement.currentTime
          this.audioElement.pause()
          this.onNewAudioAnalyzer(this.analyzer)
          this.startTime = this.audioContext.currentTime
          this.audioController = 'api'
        }
        const audioVolume = Math.max(0, 1 - this.crossfadeAmount) * this.volume
        const gainVolume = this.crossfadeAmount * this.volume
        if (this.audioGainNode !== undefined) {
          this.audioGainNode.gain.linearRampToValueAtTime(
            audioVolume,
            currentTime + this.tickMS / 1000,
          )
        }
        if (this.gainNode !== undefined) {
          this.gainNode.gain.linearRampToValueAtTime(
            gainVolume,
            currentTime + this.tickMS / 1000,
          )
        }
      }
    }
    updateLoopButton() {
      const color = this.styles.primaryColor
      const disabledColor = this.styles.secondaryColor
      const loopButtons = this.getElements('LoopButton')
      loopButtons.forEach((loopButton) => {
        if (this.loopState === 'default') {
          loopButton.innerHTML = this.loopIcon(color)
        } else if (this.loopState === 'infinite') {
          loopButton.innerHTML = this.infLoopIcon(color)
        } else if (this.loopState === 'disabled') {
          loopButton.innerHTML = this.loopIcon(disabledColor)
        }
      })
    }
    updateMetadataToggle() {
      const color = this.styles.primaryColor
      const disabledColor = this.styles.secondaryColor
      const elements = this.getElements('MetadataToggle')
      elements.forEach((element) => {
        if (this.showMetadata) {
          element.innerHTML = this.pageIcon(color)
        } else {
          element.innerHTML = this.pageIcon(disabledColor)
        }
      })
    }
    bindAudioContext() {
      if (!this.audioContext) {
        this.audioContext = new AudioContext()
        this.analyzer = this.audioContext.createAnalyser()
        this.analyzer.fftSize = 4096
        if (!this.isSafari()) {
          this.audioAnalyzer = this.audioContext.createAnalyser()
          this.audioAnalyzer.fftSize = 4096
          this.audioSource = this.audioContext.createMediaElementSource(
            this.audioElement,
          )
          this.audioSource.connect(this.audioAnalyzer)
          this.audioGainNode = this.audioContext.createGain()
          this.audioGainNode.gain.value = 1
          this.audioAnalyzer.connect(this.audioGainNode)
          this.audioGainNode.connect(this.audioContext.destination)
        }
        this.gainNode = this.audioContext.createGain()
        this.gainNode.gain.value = 0
        this.analyzer.connect(this.gainNode)
        this.gainNode.connect(this.audioContext.destination)
        this.audioContext.resume()
      }
    }
    createSource({ buffer, loop }) {
      if (this.source) {
        this.source.stop()
        this.source.disconnect()
      }
      this.source = new AudioBufferSourceNode(this.audioContext)
      this.source.connect(this.analyzer)
      this.source.addEventListener('ended', () => {
        this.onTrackEnded()
      })
      this.source.buffer = this.cloneAudioBuffer(buffer)
      if (loop?.count > 0) {
        this.source.loop = true
        this.source.loopStart = this.getLoopStart(loop)
        this.source.loopEnd = this.getLoopEnd()
      } else {
        this.source.loop = false
      }
      return this.source
    }
    isSafari() {
      const userAgent = navigator.userAgent
      const chrome = userAgent.indexOf('Chrome') > -1
      const safari = userAgent.indexOf('Safari') > -1
      return safari && !chrome
    }
    cloneAudioBuffer(fromAudioBuffer) {
      const audioBuffer = new AudioBuffer({
        length: fromAudioBuffer.length,
        numberOfChannels: fromAudioBuffer.numberOfChannels,
        sampleRate: fromAudioBuffer.sampleRate,
      })
      for (
        let channelI = 0;
        channelI < audioBuffer.numberOfChannels;
        ++channelI
      ) {
        const samples = fromAudioBuffer.getChannelData(channelI)
        audioBuffer.copyToChannel(samples, channelI)
      }
      return audioBuffer
    }
    insertIntoDom() {
      const style = (name) => `${this.uuid}-${name}`
      const trackArtSizes = {
        '': '48px',
        small: '48px',
        normal: '64px',
        large: '84px',
      }
      const metadataArtDimensions = {
        small: '150px',
        normal: '200px',
        large: '250px',
      }
      const folderArtDimensions = {
        small: '48px',
        normal: '60px',
        large: '72px',
      }
      const waveformAreaVisible =
        this.styles.showWaveform ||
        this.styles.showPlayControls ||
        this.styles.showTrackInformation
      const trackNodeSize = trackArtSizes[this.styles.trackArtSize]
      const volumeHoverKey = `VolumeContainer:hover .${style(
        'VolumeRangeContainer',
      )}`
      const uuid = this.uuid
      const TrackNodePlusGroupNode = `TrackNode + .${uuid}-GroupNode`
      const OpenGroupChildren = `GroupNode.${uuid}-Open > .${uuid}-GroupChildren`
      const OpenGroupChevron = `GroupNode.${uuid}-Open > .${uuid}-GroupAccordion .${uuid}-Chevron:before`
      const GroupChildTracks = `GroupNode .${uuid}-TrackNode`
      const GroupContentWithArt = `GroupArt + .${uuid}-GroupContent`
      const TrackPlayStateHover = `TrackNode:hover .${uuid}-TrackPlayState`
      const TrackNodePlayingPlayState = `TrackNode.${uuid}-Playing .${uuid}-TrackPlayState`
      const SelectedTrackTitle = `TrackNode.${uuid}-Selected .${uuid}-TrackTitle`
      const {
        currentlyPlayingText,
        durationText,
        trackTitleText,
        trackAlbumText,
        trackArtistText,
        trackDurationText,
        trackArtSize,
        trackBackground,
        trackBackgroundHover,
        primaryColor,
        secondaryColor,
        showTrackInformation,
        metadataLabelText,
        metadataValueText,
        metadataArtSize,
        folderTitleText,
        folderDescriptionText,
        folderBorderColor,
        folderChevronColor,
        folderArtSize,
      } = this.styles
      const showControlsContainer =
        showTrackInformation ||
        !currentlyPlayingText?.hidden ||
        !durationText?.hidden
      const largerTrackArtSize =
        trackArtSize === 'normal' || trackArtSize === 'large'
      const styles = {
        mobile: {
          PlayerContainer: `
        `,
          WaveContainer: `
          display: flex;
          flex-direction: column-reverse;
          position: relative;
          justify-content: center;
        `,
          PlayContainerMobile: `
          display: ${this.styles.showPlayControls ? 'flex' : 'none'};
          justify-content: center;
          align-items: center;
          margin: 16px 0;
        `,
          'PlayContainerMobile > button': `
          padding: 0;
          width: 50px;
          height: 50px;
          display: flex;
          justify-content: center;
          align-items: center;
          margin-bottom: 8px;
        `,
          PlayContainerDesktop: `
          display: none;
          justify-content: center;
          align-items: center;
          padding-right: 1em;
        `,
          PlayButton: `
          border: 0;
          background: transparent;
          border-radius: 500em;
          padding: 10px;
          transition: 0.2s ease-out;
        `,
          'PlayButton:hover': `
          filter: brightness(125%) contrast(75%);
        `,
          'PlayButton:active': `
          filter: brightness(75%);
        `,
          PrevButton: `
          border: 0;
          background: transparent;
          border-radius: 500em;
          padding: 10px 5px;
          transition: 0.2s ease-out;
        `,
          'PrevButton:hover': `
          filter: brightness(125%) contrast(75%);
        `,
          'PrevButton:active': `
          filter: brightness(75%);
        `,
          NextButton: `
          border: 0;
          background: transparent;
          border-radius: 500em;
          padding: 10px 5px;
          transition: 0.2s ease-out;
        `,
          'NextButton:hover': `
          filter: brightness(125%) contrast(75%);
        `,
          'NextButton:active': `
          filter: brightness(75%);
        `,
          Waveform: `
          display: ${this.styles.showWaveform ? 'block' : 'none'};
          flex: 1;
        `,
          WaveformCanvasContainer: `
          position: relative;
          height: 150px;
        `,
          ParticleContainer: `
          position: absolute;
          pointer-events: none;
          top: -50px;
          width: 100%;
          height: 205px;
          display: ${this.styles.showWaveform ? 'block' : 'none'};
        `,
          ParticleBackground: `
          display: block;
          position: absolute;
        `,
          ParticleForeground: `
          display: block;
          position: absolute;
        `,
          WaveformCanvas: `
          position: absolute;
          left: 0;
          top: 0;
        `,
          Audio: `
          display: none;
        `,
          ControlsContainer: `
          display: ${showControlsContainer ? 'flex' : 'none'};
          flex-wrap: wrap;
          padding: ${this.styles.showWaveform ? '8px' : '0px'} 8px 0 8px;
          position: relative;
        `,
          NameContainer: `
          font-size: ${currentlyPlayingText.size};
          color: ${currentlyPlayingText.color};
          font-weight: ${currentlyPlayingText.bold ? 'bold' : 'normal'};
          text-transform: ${
            currentlyPlayingText.allCaps ? 'uppercase' : 'none'
          };
          display: block;
          opacity: ${currentlyPlayingText.hidden ? '0' : '1'};
          padding: 12px 0 10px;
          flex: 1;
          text-align: center;
        `,
          FlexSpacer: `
          flex: 0;
        `,
          MetadataToggleContainer: `
          display: ${showTrackInformation ? 'flex' : 'none'};
          align-items: center;
          margin-right: 12px;
        `,
          MetadataToggle: `
          padding: 0;
          border: 0;
          background: transparent;
        `,
          VolDurContainer: `
          display: none;
          flex-direction: row;
          justify-content: flex-end;
        `,
          DurationContainer: `
          font-size: ${durationText.size};
          color: ${durationText.color};
          font-weight: ${durationText.bold ? 'bold' : 'normal'};
          text-transform: ${durationText.allCaps ? 'uppercase' : 'none'};
          display: ${durationText.hidden ? 'none' : 'flex'};
          align-items: center;
        `,
          VolumeContainer: `
          display: ${showTrackInformation ? 'flex' : 'none'};
          align-items: center;
        `,
          VolumeRangeContainer: `
          display: none;
          position: relative;
          height: 16px;
          align-items: center;
          font-family: Arial;
          font-size: 16px;
          line-height: 1;
        `,
          [volumeHoverKey]: `
          display: flex;
        `,
          VolumeButton: `
          background: transparent;
          border: 0;
          padding: 0 8px;
        `,
          LoopContainer: `
          display: ${showTrackInformation ? 'flex' : 'none'};
          align-items: center;
          margin-right: 6px;
        `,
          LoopButton: `
          background: transparent;
          border: 0;
          padding: 0 8px;
        `,
          VolumeBar: `
          position: absolute;
          left: 0px;
          height: 12px;
          top: 2px;
          border-radius: 100em;
          pointer-events: none;
          min-width: 20px;
        `,
          GroupAccordion: `
          padding: 4px 0;
          margin-bottom: 8px;
          ${
            !!folderBorderColor
              ? `border-bottom: 1px solid ${folderBorderColor};`
              : ''
          }
        `,
          GroupArt: `
        `,
          'GroupArt img': `
          width: ${folderArtDimensions[folderArtSize]};
          height: ${folderArtDimensions[folderArtSize]};
          object-fit: cover;
          margin-right: 16px;
        `,
          GroupContent: `
          display: flex;
          flex: 1;
          flex-direction: column;
          padding-right: 0.75em;
          justify-content: center;
        `,
          [GroupContentWithArt]: `
          padding-top: 4px;
        `,
          GroupTitle: `
          font-size: ${folderTitleText.size};
          color: ${folderTitleText.color};
          font-weight: ${folderTitleText.bold ? 'bold' : 'normal'};
          text-transform: ${folderTitleText.allCaps ? 'uppercase' : 'none'};
          display: ${folderTitleText.hidden ? 'none' : 'block'};
          margin-bottom: 0.25em;
        `,
          LibraryContainer: `
          display: flex;
          flex-direction: column;
        `,
          TracksContainer: `
          flex: 1;
          padding: 0 4px 0 8px;
          margin-top: 12px;
        `,
          MetadataContainer: `
          flex: 0;
          display: none;
          margin-top: 16px;
          padding: 16px;
          position: relative;
        `,
          'MetadataContainer.open': `
          display: flex;
          flex: 1;
          flex-wrap: wrap;
        `,
          MetadataCloseContainer: `
          position: absolute;
          top: -16px;
          right: 0;
        `,
          MetadataCloseButton: `
          background: transparent;
          padding: 16px;
          border: 0;
        `,
          MetadataAlbumArt: `
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 32px;
        `,
          MetadataItem: `
          text-align: left;
          min-height: 55px;
          padding: 0 8px;
        `,
          MetadataItemDescription: `
          text-align: left;
          min-height: 55px;
          padding: 0 8px;
          width: 600px;
          max-width: 100%;
        `,
          MetadataLabel: `
          font-size: ${metadataLabelText.size};
          color: ${metadataLabelText.color};
          font-weight: ${metadataLabelText.bold ? 'bold' : 'normal'};
          text-transform: ${metadataLabelText.allCaps ? 'uppercase' : 'none'};
          display: ${metadataLabelText.hidden ? 'none' : 'block'};
        `,
          MetadataValue: `
          font-size: ${metadataValueText.size};
          color: ${metadataValueText.color};
          font-weight: ${metadataValueText.bold ? 'bold' : 'normal'};
          text-transform: ${metadataValueText.allCaps ? 'uppercase' : 'none'};
          display: ${metadataValueText.hidden ? 'none' : 'block'};
          margin-bottom: 20px;
        `,
          AlbumArt: `
          width: ${metadataArtDimensions[metadataArtSize]};
          height: ${metadataArtDimensions[metadataArtSize]};
          display: ${metadataArtSize === '' ? 'none' : 'block'};
        `,
          Hide: `
          display: none;
        `,
          [TrackNodePlusGroupNode]: `
          margin-top: 8px;
         `,
          GroupChildren: `
          padding-bottom: 8px;
          
          display: none;
        `,
          [OpenGroupChildren]: `
          display: block;
        `,
          TrackNode: `
          display: flex;
          background: none;
          border: 0;
          cursor: pointer;
          align-items: center;
          text-align: left;
          padding: 0 16px 0 0;
          transition: 0.2s ease-out;
          width: 100%;
          position: relative;
          filter: none;
          align-items: stretch;
          ${!!trackArtSize ? 'margin-bottom: 4px;' : ''}
          ${trackBackground ? `background: ${trackBackground};` : ''}
        `,
          'TrackNode:hover': `
          background: ${trackBackgroundHover};
        `,
          [GroupChildTracks]: `
        `,
          TrackNumber: `
            margin-right: 1em;
            font-size: 16px;
            font-weight: bold;
            line-height: 15px;
            display: none;
        `,
          TrackArtContainer: `
          width: 40px;
          height: 40px;
          margin-right: 1rem;
          background: rgba(80,80,80,0.1);
        `,
          TrackArt: `
          width: 100%;
        `,
          TrackDescription: `
          flex: 1;
          position: relative;
          display: flex;
          flex-direction: column;
          justify-content: center;
          min-height: 58px;
        `,
          TrackTitle: `
          font-size: ${trackTitleText.size};
          color: ${trackTitleText.color};
          font-weight: ${trackTitleText.bold ? 'bold' : 'normal'};
          text-transform: ${trackTitleText.allCaps ? 'uppercase' : 'none'};
          display: ${trackTitleText.hidden ? 'none' : 'block'};
     
          transition: 0.2s ease-out;
        `,
          [SelectedTrackTitle]: `
          color: ${primaryColor};
        `,
          TrackArtist: `
          font-size: ${trackArtistText.size};
          color: ${trackArtistText.color};
          font-weight: ${trackArtistText.bold ? 'bold' : 'normal'};
          text-transform: ${trackArtistText.allCaps ? 'uppercase' : 'none'};
          display: ${trackArtistText.hidden ? 'none' : 'block'};
        `,
          TrackAlbum: `
          font-size: ${trackAlbumText.size};
          color: ${trackAlbumText.color};
          font-weight: ${trackAlbumText.bold ? 'bold' : 'normal'};
          text-transform: ${trackAlbumText.allCaps ? 'uppercase' : 'none'};
          display: ${trackAlbumText.hidden ? 'none' : 'block'};
        `,
          TrackDuration: `
          margin-left: 1rem;
          font-size: ${trackDurationText.size};
          color: ${trackDurationText.color};
          font-weight: ${trackDurationText.bold ? 'bold' : 'normal'};
          text-transform: ${trackDurationText.allCaps ? 'uppercase' : 'none'};
          display: ${trackDurationText.hidden ? 'none' : 'flex'};
          align-items: center;
        `,
          TrackProgressBar: `
            height: 2px;
            width: calc(100% + 46px);
            position: absolute;
            bottom: ${largerTrackArtSize ? '8px' : '2px'};
            background: ${secondaryColor};
        `,
          TrackProgressBarAmount: `
          position: absolute;
          left: 0;
          top: 0;
          height: 2px;
          max-width: 100%;
        `,
          TrackPlayState: `
          transition: 0.2s ease-out;
          margin-right: ${trackArtSize === '' ? '0px' : '16px'};
          height: ${trackNodeSize};
          width: ${trackNodeSize};
          display: flex;
          align-items: center;
          justify-content: center;
          background-repeat: no-repeat;
          background-size: cover;
          align-self: center;
        `,
          'TrackPlayState .PlayIcon': `
            display: flex;
            border-radius: 500em;
            width: 29px;
            height: 30px;
            padding-left: 1px;
            justify-content: center;
            align-items: center;
        `,
          'TrackPlayState.hasArt .PlayIcon': `
            background: #00000088;
        `,
          'TrackPlayState .PauseIcon': `
            display: none;
        `,
          [TrackPlayStateHover]: `
        `,
          [`${TrackNodePlayingPlayState} .PlayIcon`]: `
            display: none;
        `,
          [`${TrackNodePlayingPlayState} .PauseIcon`]: `
            display: inline-block;
        `,
          GroupToggle: `
          border: 0;
          text-align: left;
          padding: 0;
          background: transparent;
          cursor: pointer;
          width: 100%;
          display: flex;
          flex-direction: row;
          align-items: stretch;
        `,
          GroupDescription: `
          font-size: ${folderDescriptionText.size};
          color: ${folderDescriptionText.color};
          font-weight: ${folderDescriptionText.bold ? 'bold' : 'normal'};
          text-transform: ${
            folderDescriptionText.allCaps ? 'uppercase' : 'none'
          };
          display: ${folderDescriptionText.hidden ? 'none' : 'block'};
        `,
          ChevronContainer: `
          display: flex;
          align-items: center;
          justify-content: center;
          width: 40px;
        `,
          Chevron: `
          line-height: 1;
          color: ${folderChevronColor};
        `,
          'Chevron:before': `
          font-size: 24px;
          border-style: solid;
          border-width: 0.1em 0.1em 0 0;
          content: '';
          display: inline-block;
          height: 0.35em;
          left: 0.15em;
          position: relative;
          top: 0.15em;
          transform: rotate(-45deg);
          vertical-align: top;
          width: 0.35em;
        `,
          [OpenGroupChevron]: `
          top: 0;
          transform: rotate(135deg);
        `,
        },
        desktop: {
          PlayerContainer: `
            padding: ${waveformAreaVisible ? '16px' : ''} 0;
          `,
          WaveContainer: `
          flex-direction: row;
        `,
          ParticleContainer: `
          height: 250px;
        `,
          PlayContainerDesktop: `
          display: ${this.styles.showPlayControls ? 'flex' : 'none'};
        `,
          PlayContainerMobile: `
          display: none;
        `,
          LibraryContainer: `
        `,
          MetadataFirstRow: `
          flex-direction: row;
        `,
          FirstRowLeftSide: `
          padding-bottom: 0px;
        `,
          AlbumArt: `
          margin-right: 16px;
        `,
          FirstRowRightSide: `
          flex-direction: row;
        `,
          MetadataSecondRow: `
          flex-direction: row;
          padding: 32px 0 16px;
        `,
          NameContainer: `
          text-align: left;
          align-items: center;
          padding: 2px 0 0 0;
        `,
          FlexSpacer: `
          flex: 1;
        `,
          VolDurContainer: `
          display: flex;
        `,
          MetadataContainer: `
          flex: 0;
          display: none;
          margin-top: 16px;
          padding: 16px;
          position: relative;
        `,
          'MetadataContainer.open': `
          display: block;
          flex: 1;
          
        `,
          MetadataAlbumArt: `
          float: left;
          display: block;
          width: auto;
          margin: 0;
        `,
          MetadataItem: `
          text-align: left;
          min-height: 55px;
          padding: 0 8px;
          float: left;
          width: 200px;
        `,
          MetadataItemDescription: `
          text-align: left;
          min-height: 55px;
          padding: 0 8px;
          float: left;
          width: 600px;
          max-width: 100%;
        `,
        },
      }
      let trackNumber = 1
      const renderNode = (node) => {
        const firstChar = node.id.slice(0, 1)
        let children = ''
        if (node?.children?.length > 0) {
          children = `
          
            ${node.children?.map(renderNode).join('')}
          
        `
        }
        let groupDescription = ''
        if (node.data.description) {
          groupDescription = `
            
              ${node.data.description || ''}
            
`
        }
        if (firstChar === 'g') {
          this.groups[node.id] = node
          const hasArt = !!node?.data?.art
          const art = hasArt
            ? `
            `
            : ''
          trackNumber = 1
          return `
          
            
              
             
            ${children}
          
 
        `
        } else if (firstChar === 't') {
          this.tracks[node.id] = node
          this.trackOrder.push(node)
          const hasArt =
            this.styles?.trackArtSize && node?.data?.art ? 'hasArt' : ''
          const art = hasArt
            ? `style="background-image: url('${node.data.art}');"`
            : ''
          const seconds = node.data.duration / 1000
          const mins = Math.floor(seconds / 60)
          const remaining = Math.floor(seconds - mins * 60)
          const duration = `${mins}:${
            remaining > 9 ? remaining : '0' + remaining
          }`
          const currentTrackNumber = trackNumber
          trackNumber = trackNumber + 1
          const iconSize = 12
          const trackColor = node?.data?.theme?.highlightColor
          const buttonColor = this.styles.trackPlayButtonColor
          const pauseColor = node?.data?.theme?.highlightColor || buttonColor
          const playButton = this.playButton(buttonColor, iconSize, iconSize)
          const pauseButton = this.pauseButton(pauseColor, iconSize, iconSize)
          const trackProgressBar = `
            
          `
          return `
          
        `
        }
      }
      this.container.innerHTML = `
      
        
          
          
          
          
            
            
            
            
            
          
          
            
            
            
              ${data.map(renderNode).join('')}
            
           
         
       
    `
      const { mobile, desktop } = styles
      let compiledStyles = this.minifyStyles(
        Object.keys(mobile).reduce((acc, cur) => {
          acc += `.${this.uuid}-${cur} {${mobile[cur]}}`
          return acc
        }, ''),
      )
      const desktopStyles = this.minifyStyles(
        Object.keys(desktop).reduce((acc, cur) => {
          acc += `.${this.uuid}-PlayerContainer.desktop .${this.uuid}-${cur} {${desktop[cur]}}`
          return acc
        }, ''),
      )
      compiledStyles += `${desktopStyles}`
      const fontFamily = this.styles.fontFamily
      const fontFamilyParam = fontFamily.replace(/ /g, '+')
      const letterSpacing = `${this.styles.letterSpacing < 10 ? '0' : ''}${
        this.styles.letterSpacing
      }`
      document.head.insertAdjacentHTML(
        'beforeend',
        `
        `,
      )
      this.selectedTrackStyleElement = document.createElement('style')
      this.selectedTrackStyleElement.dataset.playerUuid = this.id
      document.head.appendChild(this.selectedTrackStyleElement)
    }
    minifyStyles(styles) {
      return styles
      return styles
        .replace(/([^0-9a-zA-Z\.#])\s+/g, '$1')
        .replace(/\s([^0-9a-zA-Z\.#]+)/g, '$1')
        .replace(/;}/g, '}')
        .replace(/\/\*.*?\*\//g, '')
    }
    renderMetadata(shouldRender, track) {
      const { text: title } = track
      const { artist } = track?.data || {}
      const {
        album,
        albumArtist,
        composer,
        genre,
        grouping,
        year,
        releaseDate,
        bpm,
        isrc,
        description,
      } = track?.data?.metadata || {}
      const style = (name) => `${this.uuid}-${name}`
      const container = this.getElements('MetadataContainer')[0]
      const color = this.styles.primaryColor
      const art = track?.data?.art || this.groups[track?.parent]?.data?.art
      const hideIfEmpty = (value) => {
        if (!value) {
          return style('Hide')
        }
        return ''
      }
      if (shouldRender) {
        container.classList.add('open')
        container.innerHTML = `
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    `
        this.getElements('MetadataCloseButton')[0].addEventListener(
          'click',
          () => {
            this.showMetadata = false
            this.uiDirty = true
          },
        )
      } else {
        container.classList.remove('open')
        container.innerHTML = ''
      }
    }
    generateSelectedTrackStyle() {
      const id = `mp-${this.id}`
      const uuid = this.uuid
      const color = this.styles.primaryColor
      this.selectedTrackStyleElement.innerHTML = this.minifyStyles(`
      /*********** Baseline, reset styles ***********/
      .${id} input[type="range"] {
        -webkit-appearance: none;
        appearance: none;
        background: transparent;
        cursor: pointer;
        width: 120px;
      }
      
      .${id} button[type="button"] {
        cursor: pointer;
      }
      /* Removes default focus */
      .${id} input[type="range"]:focus {
        outline: none;
      }
      /******** Chrome, Safari, Opera and Edge Chromium styles ********/
      /* slider track */
      .${id} input[type="range"]::-webkit-slider-runnable-track {
        background-color: #a8a8a8;
        border-radius: 8px;
        height: 12px;
      }
      /* slider thumb */
      .${id} input[type="range"]::-webkit-slider-thumb {
        -webkit-appearance: none; /* Override default look */
        appearance: none;
        margin-top: -4px; /* Centers thumb on the track */
        background-color: ${color};
        border-radius: 500em;
        height: 20px;
        width: 20px;
      }
      /*********** Firefox styles ***********/
      /* slider track */
      .${id} input[type="range"]::-moz-range-track {
        background-color: #a8a8a8;
        border-radius: 0.5rem;
        height: 0.5rem;
      }
      /* slider thumb */
      .${id} input[type="range"]::-moz-range-thumb {
        background-color: ${color};
        border: none; /*Removes extra border that FF applies*/
        border-radius: 0.5rem;
        height: 1rem;
        width: 1rem;
      }
      .${id} .${uuid}-VolumeBar {
        background-color: ${color};
      }
      
      .${id} .${uuid}-VolumeBar {
        background-color: ${color};
      }
      
      .${id} .${uuid}-Loader {
        display: inline-block;
        width: 50px;
        height: 30px;
        position: relative;
        left: 4px;
        top: -2px;
      }
      .${id} .${uuid}-Loader:after {
        content: " ";
        display: block;
        width: 24px;
        height: 24px;
        margin: 0px;
        border-radius: 50%;
        border: 6px solid ${color};
        border-color: ${color} transparent ${color} transparent;
        animation: mp-loading-spinner 1.2s linear infinite;
      }
      @keyframes mp-loading-spinner {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }
      
    `)
    }
    pauseButton(color, height = 30, width = 30) {
      return `
      
    `
    }
    playButton(color, height = 30, width = 30) {
      return `
      
    `
    }
    volumeOne(color) {
      return `
      
    `
    }
    volumeTwo(color) {
      return `
      
    `
    }
    volumeMute(color) {
      return `
      
    `
    }
    nextIcon(color) {
      return `
      
      `
    }
    prevIcon(color) {
      return `
      
      `
    }
    loopIcon(color) {
      return `
      
    `
    }
    pageIcon(color) {
      return `
    
    `
    }
    infLoopIcon(color) {
      return `
      
    `
    }
    loadingSpinner() {
      return ``
    }
    setLoading(loading) {
      this.loading = loading
    }
  }
  class WaveCanvas {
    constructor(element, player, data, theme, defaultTrack) {
      this.element = element
      this.data = data
      this.player = player
      this.theme = theme
      this.mousePosition = 0
      this.bindEventHandlers()
      this.defaultTrack = defaultTrack
      this.state = 'transition-in'
      this.transitionTime = 0
      this.isMobile = false
      this.volume = 1
      this.previousState = element
        .getContext('2d')
        .getImageData(0, 0, 1500, 500)
      window.requestAnimationFrame(() => this.draw())
      this.transitionComplete = () => {
        return new Promise((resolve, reject) => {
          this.resolveTransition = resolve
        })
      }
    }
    setAudioAnalyzer(audioAnalyzer) {
      this.audioAnalyzer = audioAnalyzer
    }
    setVolume(vol) {
      this.volume = vol
    }
    draw() {
      const transitionDuration = 0.1
      const canvas = this.element
      const width = canvas.parentElement.clientWidth
      const height = canvas.parentElement.clientHeight
      if (this.isMobile) {
        this.mousePosition = 0
      }
      if (width && height) {
        const ctx = canvas.getContext('2d')
        const currentTime = this.player.getCurrentTime()
        const duration = this.player.getDuration()
        const timeProgress = currentTime / duration
        const hovered = this.mousePosition > 0
        const mousePositionRelative = this.mousePosition / width
        const mouseAhead = mousePositionRelative > timeProgress
        const analyzer = this.audioAnalyzer
        const isPlaying = this.player.isPlaying()
        canvas.width = width
        canvas.height = height
        const { bgColor, fgColor, highlightColor, visualizer } = this.theme
        switch (this.state) {
          case 'transition-in':
            const transitionProgress = this.transitionTime / transitionDuration
            const transitionWidth = transitionProgress * width
            if (this.previousState) {
              ctx.putImageData(this.previousState, 0, 0)
            }
            ctx.clearRect(
              0,
              0,
              this.easeOutQuad(transitionProgress, 0, 1, 1) * width,
              500,
            )
            this.drawWaveform(
              canvas,
              0,
              this.easeOutQuad(transitionProgress, 0, 1, 1),
              {
                color: this.defaultTrack ? highlightColor : bgColor,
              },
            )
            this.transitionTime += 0.002
            if (this.transitionTime > transitionDuration) {
              this.state = 'active'
            }
            break
          case 'active':
            ctx.clearRect(0, 0, 1500, 500)
            if (currentTime > 0) {
              let dataArray = []
              let visualizerArray = []
              if (analyzer) {
                const bufferLength = analyzer.frequencyBinCount
                dataArray = new Uint8Array(bufferLength)
                analyzer.getByteFrequencyData(dataArray)
                visualizerArray = dataArray.slice(0, 1700)
              }
              let progressStart = 0
              let backgroundStart = timeProgress
              let hoverStart = mousePositionRelative
              let progressEnd =
                mousePositionRelative > 0 ? mousePositionRelative : timeProgress
              let backgroundEnd = 1
              let hoverEnd = timeProgress
              if (mouseAhead) {
                hoverStart = timeProgress
                hoverEnd = mousePositionRelative
                backgroundStart = mousePositionRelative
                progressEnd = timeProgress
              }
              this.drawWaveform(
                canvas,
                backgroundStart,
                backgroundEnd,
                {
                  color: bgColor,
                  visualizer,
                },
                visualizerArray,
              )
              this.drawWaveform(
                canvas,
                progressStart,
                progressEnd,
                {
                  color: highlightColor,
                  visualizer,
                },
                visualizerArray,
              )
              if (this.mousePosition > 0) {
                this.drawWaveform(
                  canvas,
                  hoverStart,
                  hoverEnd,
                  {
                    color: fgColor,
                    visualizer,
                  },
                  visualizerArray,
                )
              }
            } else {
              if (this.defaultTrack) {
                this.drawWaveform(
                  canvas,
                  Math.max(mousePositionRelative, timeProgress, 0),
                  1,
                  {
                    color: hovered ? fgColor : highlightColor,
                    visualizer,
                  },
                )
                if (this.mousePosition > 0) {
                  this.drawWaveform(canvas, 0, mousePositionRelative, {
                    color: highlightColor,
                    visualizer,
                  })
                }
              } else {
                this.drawWaveform(canvas, 0, 1, {
                  color: bgColor,
                  visualizer,
                })
              }
            }
            break
        }
      }
      window.requestAnimationFrame(() => this.draw())
    }
    drawWaveform(canvas, startPosition, endPosition, theme, freqPoints = []) {
      const data = this.data
      const ctx = canvas.getContext('2d')
      const width = canvas.clientWidth
      const offset = 75.5
      const timeFactor = width / data.length
      const drawStart = Math.floor(data.length * startPosition)
      const drawEnd = Math.ceil(data.length * endPosition)
      const { color, visualizer } = theme
      ctx.beginPath()
      for (let x = drawStart; x < drawEnd; x += 1) {
        if (data[x] !== undefined) {
          const val = Number(data[x]?.[1]) * getFrequencyModifier(x)
          const y = offset + this.clampWaveValue(Math.round(val))
          ctx.lineTo(x * timeFactor, y)
        }
      }
      for (let x = drawEnd - 1; x >= drawStart; x -= 1) {
        if (data[x] !== undefined) {
          const val = Number(data[x]?.[0]) * getFrequencyModifier(x)
          const y = offset + this.clampWaveValue(Math.round(val))
          ctx.lineTo(x * timeFactor, y)
        }
      }
      ctx.fillStyle = color
      ctx.strokeStyle = color
      ctx.closePath()
      ctx.stroke()
      ctx.fill()
      function getFrequencyModifier(x) {
        let baseMultiplier = 70
        if (visualizer === 'default') {
          if (freqPoints.length > 0) {
            baseMultiplier = 35
            const chunkSize = Math.ceil(data.length / freqPoints.length)
            return (
              (1 + (freqPoints[Math.floor(x / chunkSize)] / 255) * 1.75) *
              baseMultiplier
            )
          }
        } else if (visualizer === 'orchestral') {
          if (freqPoints.length > 0) {
            baseMultiplier = 60
            const chunkSize = Math.ceil(data.length / freqPoints.length)
            return (
              (0.6 + (freqPoints[Math.floor(x / chunkSize)] / 255) * 1.75) *
              baseMultiplier
            )
          }
        } else if (visualizer === 'bass') {
          if (freqPoints.length > 0) {
            //freqPoints[5] is aprox 55-65hz
            baseMultiplier = 80
            const chunkSize = Math.ceil(data.length / freqPoints.length)
            return (
              (0.25 +
                freqPoints[5] / 255 +
                freqPoints[Math.floor(x / chunkSize)] / 255) *
              baseMultiplier
            )
          }
        }
        return baseMultiplier
      }
    }
    bindEventHandlers() {
      this.element.addEventListener('mouseleave', (evt) => {
        this.mousePosition = 0
      })
      this.element.addEventListener('touchcancel', (evt) => {
        this.isMobile = true
        this.mousePosition = 0
      })
      this.element.addEventListener('touchend', (evt) => {
        this.isMobile = true
        this.mousePosition = 0
      })
      this.element.addEventListener('mousemove', (evt) => {
        this.mousePosition = evt.offsetX
      })
      this.element.addEventListener('touchmove', (evt) => {
        this.mousePosition = Math.min(
          Math.max(
            0,
            evt.touches[0].clientX - evt.target.getBoundingClientRect().left,
          ),
          evt.target.clientWidth,
        )
      })
    }
    easeOutQuad(t, b, c, d) {
      return -c * (t /= d) * (t - 2) + b
    }
    clampWaveValue(x) {
      const sign = Math.sign(x)
      const abs = Math.abs(x) / 100
      if (abs > 0.7) {
        const value = 1 - (1 - abs) * (1 - abs)
        return sign * value * 74
      } else {
        return x
      }
    }
    sum(arr) {
      return arr.reduce((acc, cur) => {
        return acc + cur
      }, 0)
    }
  }
  init()
  function getWaveformData(waveformDataBuffer) {
    /**
     * Provides access to the waveform data for a single audio channel.
     */
    function WaveformDataChannel(waveformData, channelIndex) {
      this._waveformData = waveformData
      this._channelIndex = channelIndex
    }
    /**
     * Returns the waveform minimum at the given index position.
     */
    WaveformDataChannel.prototype.min_sample = function (index) {
      var offset =
        (index * this._waveformData.channels + this._channelIndex) * 2
      return this._waveformData._at(offset)
    }
    /**
     * Returns the waveform maximum at the given index position.
     */
    WaveformDataChannel.prototype.max_sample = function (index) {
      var offset =
        (index * this._waveformData.channels + this._channelIndex) * 2 + 1
      return this._waveformData._at(offset)
    }
    /**
     * Sets the waveform minimum at the given index position.
     */
    WaveformDataChannel.prototype.set_min_sample = function (index, sample) {
      var offset =
        (index * this._waveformData.channels + this._channelIndex) * 2
      return this._waveformData._set_at(offset, sample)
    }
    /**
     * Sets the waveform maximum at the given index position.
     */
    WaveformDataChannel.prototype.set_max_sample = function (index, sample) {
      var offset =
        (index * this._waveformData.channels + this._channelIndex) * 2 + 1
      return this._waveformData._set_at(offset, sample)
    }
    /**
     * Returns all the waveform minimum values as an array.
     */
    WaveformDataChannel.prototype.min_array = function () {
      var length = this._waveformData.length
      var values = []
      for (var i = 0; i < length; i++) {
        values.push(this.min_sample(i))
      }
      return values
    }
    /**
     * Returns all the waveform maximum values as an array.
     */
    WaveformDataChannel.prototype.max_array = function () {
      var length = this._waveformData.length
      var values = []
      for (var i = 0; i < length; i++) {
        values.push(this.max_sample(i))
      }
      return values
    }
    /**
     * Provides access to waveform data.
     */
    function WaveformData(data) {
      this._data = new DataView(data)
      this._offset = this._version() === 2 ? 24 : 20
      this._channels = []
      for (var channel = 0; channel < this.channels; channel++) {
        this._channels[channel] = new WaveformDataChannel(this, channel)
      }
    }
    var defaultOptions = {
      scale: 512,
      amplitude_scale: 1.0,
      split_channels: false,
      disable_worker: false,
    }
    function getOptions(options) {
      var opts = {
        scale: options.scale || defaultOptions.scale,
        amplitude_scale:
          options.amplitude_scale || defaultOptions.amplitude_scale,
        split_channels: options.split_channels || defaultOptions.split_channels,
        disable_worker: options.disable_worker || defaultOptions.disable_worker,
      }
      return opts
    }
    function getChannelData(audio_buffer) {
      var channels = []
      for (var i = 0; i < audio_buffer.numberOfChannels; ++i) {
        channels.push(audio_buffer.getChannelData(i).buffer)
      }
      return channels
    }
    /**
     * Creates and returns a WaveformData instance from the given waveform data.
     */
    WaveformData.create = function create(data) {
      return new WaveformData(data)
    }
    WaveformData.prototype = {
      _getResampleOptions(options) {
        var opts = {}
        opts.scale = options.scale
        opts.width = options.width
        if (
          opts.width != null &&
          (typeof opts.width !== 'number' || opts.width <= 0)
        ) {
          throw new RangeError(
            'WaveformData.resample(): width should be a positive integer value',
          )
        }
        if (
          opts.scale != null &&
          (typeof opts.scale !== 'number' || opts.scale <= 0)
        ) {
          throw new RangeError(
            'WaveformData.resample(): scale should be a positive integer value',
          )
        }
        if (!opts.scale && !opts.width) {
          throw new Error(
            'WaveformData.resample(): Missing scale or width option',
          )
        }
        if (opts.width) {
          // Calculate the target scale for the resampled waveform
          opts.scale = Math.floor(
            (this.duration * this.sample_rate) / opts.width,
          )
        }
        if (opts.scale < this.scale) {
          throw new Error(
            'WaveformData.resample(): Zoom level ' +
              opts.scale +
              ' too low, minimum: ' +
              this.scale,
          )
        }
        opts.abortSignal = options.abortSignal
        return opts
      },
      resample: function (options) {
        options = this._getResampleOptions(options)
        options.waveformData = this
        var resampler = new WaveformResampler(options)
        while (!resampler.next()) {
          // nothing
        }
        return new WaveformData(resampler.getOutputData())
      },
      /**
       * Concatenates with one or more other waveforms, returning a new WaveformData object.
       */
      concat: function () {
        var self = this
        var otherWaveforms = Array.prototype.slice.call(arguments)
        // Check that all the supplied waveforms are compatible
        otherWaveforms.forEach(function (otherWaveform) {
          if (
            self.channels !== otherWaveform.channels ||
            self.sample_rate !== otherWaveform.sample_rate ||
            self.bits !== otherWaveform.bits ||
            self.scale !== otherWaveform.scale
          ) {
            throw new Error('WaveformData.concat(): Waveforms are incompatible')
          }
        })
        var combinedBuffer = this._concatBuffers.apply(this, otherWaveforms)
        return WaveformData.create(combinedBuffer)
      },
      /**
       * Returns a new ArrayBuffer with the concatenated waveform.
       * All waveforms must have identical metadata (version, channels, etc)
       */
      _concatBuffers: function () {
        var otherWaveforms = Array.prototype.slice.call(arguments)
        var headerSize = this._offset
        var totalSize = headerSize
        var totalDataLength = 0
        var bufferCollection = [this].concat(otherWaveforms).map(function (w) {
          return w._data.buffer
        })
        var i, buffer
        for (i = 0; i < bufferCollection.length; i++) {
          buffer = bufferCollection[i]
          var dataSize = new DataView(buffer).getInt32(16, true)
          totalSize += buffer.byteLength - headerSize
          totalDataLength += dataSize
        }
        var totalBuffer = new ArrayBuffer(totalSize)
        var sourceHeader = new DataView(bufferCollection[0])
        var totalBufferView = new DataView(totalBuffer)
        // Copy the header from the first chunk
        for (i = 0; i < headerSize; i++) {
          totalBufferView.setUint8(i, sourceHeader.getUint8(i))
        }
        // Rewrite the data-length header item to reflect all of the samples concatenated together
        totalBufferView.setInt32(16, totalDataLength, true)
        var offset = 0
        var dataOfTotalBuffer = new Uint8Array(totalBuffer, headerSize)
        for (i = 0; i < bufferCollection.length; i++) {
          buffer = bufferCollection[i]
          dataOfTotalBuffer.set(new Uint8Array(buffer, headerSize), offset)
          offset += buffer.byteLength - headerSize
        }
        return totalBuffer
      },
      /**
       * Returns the data format version number.
       */
      _version: function () {
        return this._data.getInt32(0, true)
      },
      /**
       * Returns the length of the waveform, in pixels.
       */
      get length() {
        return this._data.getUint32(16, true)
      },
      /**
       * Returns the number of bits per sample, either 8 or 16.
       */
      get bits() {
        var bits = Boolean(this._data.getUint32(4, true))
        return bits ? 8 : 16
      },
      /**
       * Returns the (approximate) duration of the audio file, in seconds.
       */
      get duration() {
        return (this.length * this.scale) / this.sample_rate
      },
      /**
       * Returns the number of pixels per second.
       */
      get pixels_per_second() {
        return this.sample_rate / this.scale
      },
      /**
       * Returns the amount of time represented by a single pixel, in seconds.
       */
      get seconds_per_pixel() {
        return this.scale / this.sample_rate
      },
      /**
       * Returns the number of waveform channels.
       */
      get channels() {
        if (this._version() === 2) {
          return this._data.getInt32(20, true)
        } else {
          return 1
        }
      },
      /**
       * Returns a waveform channel.
       */
      channel: function (index) {
        if (index >= 0 && index < this._channels.length) {
          return this._channels[index]
        } else {
          throw new RangeError('Invalid channel: ' + index)
        }
      },
      /**
       * Returns the number of audio samples per second.
       */
      get sample_rate() {
        return this._data.getInt32(8, true)
      },
      /**
       * Returns the number of audio samples per pixel.
       */
      get scale() {
        return this._data.getInt32(12, true)
      },
      /**
       * Returns a waveform data value at a specific offset.
       */
      _at: function at_sample(index) {
        if (this.bits === 8) {
          return this._data.getInt8(this._offset + index)
        } else {
          return this._data.getInt16(this._offset + index * 2, true)
        }
      },
      /**
       * Sets a waveform data value at a specific offset.
       */
      _set_at: function set_at(index, sample) {
        if (this.bits === 8) {
          return this._data.setInt8(this._offset + index, sample)
        } else {
          return this._data.setInt16(this._offset + index * 2, sample, true)
        }
      },
      /**
       * Returns the waveform data index position for a given time.
       */
      at_time: function at_time(time) {
        return Math.floor((time * this.sample_rate) / this.scale)
      },
      /**
       * Returns the time in seconds for a given index.
       */
      time: function time(index) {
        return (index * this.scale) / this.sample_rate
      },
      /**
       * Returns an object containing the waveform data.
       */
      toJSON: function () {
        const waveform = {
          version: 2,
          channels: this.channels,
          sample_rate: this.sample_rate,
          samples_per_pixel: this.scale,
          bits: this.bits,
          length: this.length,
          data: [],
        }
        for (var i = 0; i < this.length; i++) {
          for (var channel = 0; channel < this.channels; channel++) {
            waveform.data.push(this.channel(channel).min_sample(i))
            waveform.data.push(this.channel(channel).max_sample(i))
          }
        }
        return waveform
      },
      /**
       * Returns the waveform data in binary format as an ArrayBuffer.
       */
      toArrayBuffer: function () {
        return this._data.buffer
      },
    }
    const wfd = new WaveformData(waveformDataBuffer)
    return wfd.toJSON()
  }
  const ENERGY_LOW_THRESHOLD = 0.45
  const ENERGY_HIGH_THRESHOLD = 0.55
  class Particle {
    constructor({
      color,
      gravity,
      image,
      lifetime,
      rotation,
      rotationRate,
      scale,
      scaleRate,
      shape,
      wobble,
      position,
      velocity,
      emitter,
      trail,
      edgeBehaviour,
      svg,
      drag,
      ctx,
      bufferCanvas,
      bufferContext,
    }) {
      const scaleRender = Range.render(scale)
      this.position = position || new Vector(0, 0)
      this.velocity = velocity || new Vector(0, 0)
      this.gravity = gravity
      this.rotation = Choose.render(Range.render(rotation))
      this.rotationRate = Range.render(rotationRate)
      this.scale = new Vector(scaleRender)
      this.scaleRate = new Vector(Range.render(scaleRate))
      this.drag = Range.render(drag)
      this.lifetime = lifetime
      this.wobble = wobble ? Range.render(wobble) / 10000 : 0
      this.shape = Choose.render(shape)
      this.color = Choose.render(color)
      this.trail = trail
      this.edgeBehaviour = edgeBehaviour
      this.ctx = ctx
      this.canvas = ctx.canvas
      this.firstDraw = Date.now()
      this.lastDraw = Date.now()
      this.timeLived = 0
      this.emitter = emitter
      this.wobbleTimer = -Math.PI + Math.random() * Math.PI * 2
      if (image) {
        const img = new Image()
        img.src = Choose.render(image)
        this.image = img
      }
      this.svg = svg
      if (shape.indexOf('waveform') !== -1) {
        this.path = this.getWaveformPath(shape)
      }
      this.previousPositions = []
      this.bufferCanvas = bufferCanvas
      this.bufferContext = bufferContext
      this.leftEdge = Math.random() * 20
      this.rightEdge = this.canvas.width - Math.random() * 60
      this.topEdge = Math.random() * 20
      this.bottomEdge = this.canvas.height - Math.random() * 20
    }
    move({ delta, energy }) {
      const now = Date.now()
      const dilatedDelta = this.emitter.applyTimeDilation({ delta, energy })
      const impulse = this.emitter.getImpulse(energy)
      const current = {
        position: this.position,
        rotation: this.rotation,
        scale: this.scale,
      }
      if (this.trail.length > 0) {
        this.storeState()
      }
      let acceleration = new Vector(0, this.gravity)
      acceleration = acceleration.add(
        this.emitter.applyGravityPoints({
          position: this.position,
          delta,
          energy,
        }),
      )
      this.velocity = this.velocity.add(acceleration.multiply(dilatedDelta))
      this.velocity = this.velocity.add(
        new Vector(dilatedDelta * this.wobble * Math.sin(this.wobbleTimer), 0),
      )
      this.velocity = this.velocity.add(impulse)
      this.velocity = this.velocity.multiply(
        new Vector(1, 1).add(this.drag.multiply(-0.01 * dilatedDelta)),
      )
      this.velocity = this.handleReflection()
      this.position = this.position.add(this.velocity.multiply(dilatedDelta))
      this.rotation = this.rotation + this.rotationRate * dilatedDelta
      this.scale = this.scale.add(this.scaleRate.multiply(dilatedDelta))
      this.timeLived = this.timeLived + dilatedDelta * 10
      this.lastDraw = now
      this.wobbleTimer += dilatedDelta / 100
      this.edgeStop(current)
    }
    handleReflection() {
      if (this.edgeBehaviour === 'bounce') {
        if (this.position.y > this.canvas.height) {
          const normal = new Vector(0, -1)
          return this.velocity.subtract(
            normal.multiply(2 * this.velocity.dotProduct(normal)),
          )
        } else if (this.position.x > this.canvas.width) {
          const normal = new Vector(1, 0)
          return this.velocity.subtract(
            normal.multiply(2 * this.velocity.dotProduct(normal)),
          )
        } else if (this.position.y < 0) {
          const normal = new Vector(0, 1)
          return this.velocity.subtract(
            normal.multiply(2 * this.velocity.dotProduct(normal)),
          )
        } else if (this.position.x < 0) {
          const normal = new Vector(-1, 0)
          return this.velocity.subtract(
            normal.multiply(2 * this.velocity.dotProduct(normal)),
          )
        }
      }
      return this.velocity
    }
    edgeStop({ position, scale, rotation }) {
      if (this.edgeBehaviour === 'stopBottom') {
        if (this.position.y > this.bottomEdge) {
          this.position = position
          this.scale = scale
          this.rotation = rotation
          this.velocity = new Vector(0, 0)
          this.rotationRate = 0
          this.wobble = 0
        }
      }
    }
    getEdgeAlpha() {
      if (this.edgeBehaviour === 'fade') {
        const alpha = Math.min(
          1,
          (this.bottomEdge - this.position.y) / 25,
          (this.rightEdge - this.position.x) / 25,
          (this.position.y - this.topEdge) / 25,
          (this.position.x - this.leftEdge) / 25,
        )
        return alpha < 0 ? 0 : alpha
      }
      return 1
    }
    addImpulse(impulse) {
      this.impulse = this.impulse.add(impulse)
    }
    preDraw(ctx) {
      ctx.globalAlpha = this.getEdgeAlpha()
      ctx.setTransform(
        this.scale.x,
        0,
        0,
        this.scale.y,
        this.position.x,
        this.position.y,
      )
      ctx.rotate(this.rotation)
    }
    postDraw(ctx) {
      ctx.globalAlpha = 1
      ctx.rotate(0)
      ctx.setTransform(1, 0, 0, 1, 0, 0)
    }
    draw() {
      const ctx = this.ctx
      if (this.trail.length > 0) {
        this.drawTrail(ctx)
      }
      this.preDraw(ctx)
      switch (this.shape) {
        case 'line':
          this.drawLine(ctx)
          break
        case 'square':
          this.drawSquare(ctx)
          break
        case 'roundSquare':
          this.drawRoundSquare(ctx)
          break
        case 'circle':
          this.drawCircle(ctx)
          break
        case 'flame':
          this.drawFlame(ctx)
          break
        case 'image':
          this.drawImage(ctx)
          break
        case 'svg':
          this.drawSvg(ctx)
          break
        case 'waveformExact':
          this.drawPath(ctx)
          break
        case 'waveformClose':
        case 'waveformFast':
        case 'waveformVeryFast':
        case 'waveformLoose':
        case 'waveformVeryLoose':
          this.drawPathRound(ctx)
          break
      }
      this.postDraw(ctx)
    }
    getLifetimeRatio() {
      return this.timeLived / this.lifetime
      return (this.lastDraw - this.firstDraw) / this.lifetime
    }
    drawPath(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.beginPath()
      this.path.forEach(([x, y]) => {
        ctx.lineTo(x, y)
      })
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.closePath()
      ctx.stroke()
      ctx.fill()
    }
    drawPathRound(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.beginPath()
      ctx.moveTo(this.path[0][0], this.path[0][1])
      let i = 0
      for (i = 1; i < this.path.length - 2; i++) {
        const xc = (this.path[i][0] + this.path[i + 1][0]) / 2
        const yc = (this.path[i][1] + this.path[i + 1][1]) / 2
        ctx.quadraticCurveTo(this.path[i][0], this.path[i][1], xc, yc)
      }
      // curve through the last two points
      ctx.quadraticCurveTo(
        this.path[i][0],
        this.path[i][1],
        this.path[i + 1][0],
        this.path[i + 1][1],
      )
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.closePath()
      ctx.stroke()
      ctx.fill()
    }
    drawSvg(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      const basePath = new Path2D()
      const path = new Path2D(this.svg)
      basePath.addPath(path, {}) //{ a: 1 / 40, d: 1 / 40, e: -12.5, f: -12.5 })
      ctx.fill(path)
    }
    drawLine(ctx) {
      const lineEnd = this.velocity.multiply(4)
      const color = this.color.render(this.getLifetimeRatio())
      ctx.strokeStyle = `rgba(${color})`
      ctx.beginPath() // Start a new path
      ctx.moveTo(-lineEnd.x / 2, -lineEnd.y / 2)
      ctx.lineTo(lineEnd.x / 2, lineEnd.y / 2)
      ctx.lineWidth = 3
      ctx.stroke() // Render the path
      ctx.closePath()
    }
    drawSquare(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.fillRect(-0.5, -0.5, 1, 1)
    }
    drawRoundSquare(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.beginPath()
      ctx.roundRect(-0.5, -0.5, 1, 1, 0.1)
      ctx.fill()
      ctx.closePath()
    }
    drawFlame(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.beginPath()
      const path = new Path2D(
        'M.5176,1.0035A.3782.3782,0,0,0,.734.7219.453.453,0,0,0,.69.4064.8332.8332,0,0,0,.5306.1911,1.4556,1.4556,0,0,0,.2986.0028C.2937,0,.2936-.0006.2932.0011l-.0017.01C.2894.0257.2841.0543.28.0743A.7921.7921,0,0,1,.1662.3568C.15.3812.136.4.1.4469A.5572.5572,0,0,0,.03.552.3045.3045,0,0,0,.0011.656a.3864.3864,0,0,0,0,.0627.3462.3462,0,0,0,.099.1968A.3392.3392,0,0,0,.25,1.01.6552.6552,0,0,0,.5176,1.0035Z',
      )
      ctx.fill(path)
      ctx.closePath()
    }
    drawCircle(ctx) {
      const color = this.color.render(this.getLifetimeRatio())
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      ctx.beginPath()
      ctx.arc(-0.5, -0.5, 1, 0, 2 * Math.PI)
      ctx.fill()
      ctx.closePath()
    }
    drawImage(ctx) {
      const image = this.image
      const color = this.color.getColor(this.getLifetimeRatio())
      const colorNoAlpha = new Color(color.red, color.green, color.blue, 1)
      const c = this.bufferCanvas
      const cctx = this.bufferContext
      const x = -image.width / 2
      const y = -image.height / 2
      if (!image.height || !image.width) {
        return
      }
      if (color.isWhite()) {
        ctx.globalAlpha = ctx.globalAlpha * color.alpha
        return ctx.drawImage(image, x, y)
      }
      c.width = image.width
      c.height = image.height
      cctx.clearRect(0, 0, image.width, image.height)
      cctx.drawImage(image, 0, 0)
      cctx.globalCompositeOperation = 'source-atop'
      cctx.fillStyle = `rgba(${colorNoAlpha.render()}`
      cctx.fillRect(0, 0, image.width, image.height)
      cctx.globalCompositeOperation = 'source-over'
      ctx.globalAlpha = ctx.globalAlpha * color.alpha
      ctx.drawImage(image, x, y)
      ctx.globalCompositeOperation = 'color'
      ctx.drawImage(c, x, y)
      ctx.globalCompositeOperation = 'source-over'
    }
    drawTrail(ctx) {
      const color = this.trail.color.render(this.getLifetimeRatio())
      const edgeAlpha = this.getEdgeAlpha()
      ctx.fillStyle = `rgba(${color})`
      ctx.strokeStyle = `rgba(${color})`
      let step = 1 / (this.previousPositions.length + 1)
      let modifier = 1 - step
      this.previousPositions.forEach(({ position, rotation }) => {
        ctx.globalAlpha = modifier * edgeAlpha
        ctx.setTransform(
          this.scale.x * modifier * this.trail.size,
          0,
          0,
          this.scale.y * modifier * this.trail.size,
          position.x,
          position.y,
        )
        ctx.rotate(this.rotation)
        //ctx.beginPath()
        //ctx.arc(-0.5, -0.5, 1, 0, 2 * Math.PI)
        //ctx.fill()
        //ctx.closePath()
        ctx.fillRect(-0.5, -0.5, 1, 1)
        modifier = modifier - step
      })
    }
    getWaveformPath(shape) {
      const container = window.lastWaveformDraw || { top: [], bottom: [] }
      const points = [...container.top, ...container.bottom]
      const filterAmounts = {
        waveformExact: 1,
        waveformClose: 2,
        waveformFast: 4,
        waveformVeryFast: 8,
        waveformLoose: 16,
        waveformVeryLoose: 32,
      }
      const pointsFiltered = points.filter(
        (any, index) => index % filterAmounts[shape] === 0,
      )
      return pointsFiltered.map(([x, y]) => {
        return [x - this.canvas.width / 2, y - this.canvas.height / 2]
      })
    }
    storeState() {
      this.previousPositions = [
        {
          position: this.position,
          rotation: this.rotation,
        },
        ...this.previousPositions.slice(0, this.trail.length - 1),
      ]
    }
  }
  class Emitter {
    constructor({
      angle,
      position,
      particle,
      speed,
      rate,
      ctx,
      effects,
      system,
    }) {
      this.angle = angle
      this.position = position
      this.particle = particle
      this.rate = rate
      this.speed = speed
      this.ctx = ctx
      this.timer = rate - 10
      this.effects = effects
      this.bufferCanvas = null
      this.bufferContext = null
      this.system = system
    }
    tick({ delta, energy, max }) {
      const rate = this.rate
      const dilatedDelta = this.applyTimeDilation({ delta, energy })
      const spawnedParticles = []
      if (dilatedDelta > 0 && max > 0) {
        const amountToSpawn =
          rate > 10 ? 1 : Math.max(1, 10 - rate * (1 / dilatedDelta))
        if (this.timer > rate - 10) {
          for (let i = 0; i < amountToSpawn; i++) {
            spawnedParticles.push(this.emit(energy))
          }
          this.timer = 0
        }
        this.timer += dilatedDelta
      }
      return spawnedParticles
    }
    applyTimeDilation({ delta, energy }) {
      let value = delta
      const effects = this.effects.filter((effect) => effect.type === 'time')
      effects.forEach((effect) => {
        const factor = getEffectStrength(effect, 1, energy)
        value = value * factor
      })
      return value
    }
    emit(energy) {
      const width = this.ctx.canvas.clientWidth
      const height = this.ctx.canvas.clientHeight
      const canvasDimensions = new Vector(width, height)
      const angle = Range.render(this.angle)
      let position = Range.render(this.position).multiply(canvasDimensions)
      const spawnOffsetEffects = this.effects.filter(
        (effect) => effect.type === 'spawnOffset',
      )
      spawnOffsetEffects.forEach((effect) => {
        const offset = getEffectStrengthVector(effect, new Vector(0, 0), energy)
        position = position.add(offset.multiply(canvasDimensions))
      })
      const velocity = Vector.fromAngle(angle, Range.render(this.speed))
      let lifetime = Range.render(this.particle.lifetime) * 1000
      const lifetimeEffects = this.effects.filter(
        (effect) => effect.type === 'lifetime',
      )
      lifetimeEffects.forEach((effect) => {
        const strength = getEffectStrength(effect, 1, energy)
        lifetime = lifetime * strength
      })
      return new Particle({
        ...this.particle,
        lifetime,
        velocity,
        position,
        ctx: this.ctx,
        emitter: this,
        bufferCanvas: this.bufferCanvas,
        bufferContext: this.bufferContext,
      })
    }
    getImpulse(energy) {
      const effects = this.effects.filter((effect) => effect.type === 'impulse')
      let impulse = new Vector(0, 0)
      effects.forEach((effect) => {
        const strength = getEffectStrength(effect, 0, energy)
        impulse = impulse.add(Vector.fromAngle(effect.data.direction, strength))
      })
      return impulse
    }
    setBufferCanvas(bufferCanvas, bufferContext) {
      this.bufferContext = bufferContext
      this.bufferCanvas = bufferCanvas
    }
    applyGravityPoints({ position, delta, energy }) {
      let accel = new Vector(0, 0)
      const points = this.effects.filter((effect) =>
        this.filterEffectsByEnergy(effect, 'gravity', energy),
      )
      points.forEach((point) => {
        const { position: pointPosition, radius } = point.data
        const width = this.ctx.canvas.clientWidth
        const height = this.ctx.canvas.clientHeight
        const center = new Vector(
          pointPosition[0] * width,
          pointPosition[1] * height,
        )
        if (pointInCircle(position, center, radius * width)) {
          const strength = getEffectStrength(point, 0, energy)
          const diff = new Vector(center.x - position.x, center.y - position.y)
          const magnitude = diff.getMagnitude()
          const normal = new Vector(diff.x / magnitude, diff.y / magnitude)
          const forceFactor =
            100000 / Math.max(20000, diff.x * diff.x + diff.y * diff.y)
          accel = accel.add(normal.multiply(delta * strength * forceFactor))
        }
      })
      return accel
    }
    filterEffectsByEnergy(effect, type, energy) {
      const energyValid =
        effect.energy === 'all' ||
        (effect.energy === 'below' && energy < ENERGY_LOW_THRESHOLD) ||
        (effect.energy === 'above' && energy > ENERGY_HIGH_THRESHOLD)
      return effect.type === type && energyValid
    }
  }
  function pointInCircle(point, center, radius) {
    return (
      Math.pow(center.x - point.x, 2) + Math.pow(center.y - point.y, 2) <=
      Math.pow(radius, 2)
    )
  }
  class ParticleSystem {
    constructor({ background, foreground, emitters, player, waveData }) {
      this.background = background
      this.foreground = foreground
      this.maxParticles = 4000
      this.particles = []
      this.emitters = emitters
      this.energies = []
      this.enabled = false
      this.bgCtx = this.background.getContext('2d')
      this.fgCtx = this.foreground.getContext('2d')
      this.energyIntensity = null
      this.lastDraw = Date.now()
      this.player = player
      this.waveData = waveData
      this.bufferCanvas = document.createElement('canvas')
      this.bufferContext = this.bufferCanvas.getContext('2d')
      this.emitters.forEach((emitter) =>
        emitter.setBufferCanvas(this.bufferCanvas, this.bufferContext),
      )
      this.resetDimensions()
      this.queue()
    }
    resetDimensions() {
      this.background.width = this.background.parentElement.clientWidth
      this.background.height = this.background.parentElement.clientHeight
      this.foreground.width = this.foreground.parentElement.clientWidth
      this.foreground.height = this.foreground.parentElement.clientHeight
      this.width = this.background.width
      this.height = this.background.height
    }
    start() {
      this.enabled = true
    }
    stop() {
      this.enabled = false
      this.clear()
    }
    tick() {
      this.resetDimensions()
      this.clear()
      if (this.enabled) {
        this.update()
        this.draw()
      }
      this.queue()
    }
    clear() {
      this.bgCtx.clearRect(0, 0, this.width, this.height)
      this.fgCtx.clearRect(0, 0, this.width, this.height)
    }
    update() {
      const now = Date.now()
      let delta = (now - this.lastDraw) / 10
      const energy = this.getEnergy()
      this.spawnParticles({ delta, energy })
      this.moveParticles({ delta, energy })
      this.lastDraw = now
    }
    getEnergy() {
      const player = this.player
      const { data, average, min } = this.waveData || {}
      if (data) {
        try {
          const index = Math.ceil(
            (player.getCurrentTime() / player.getDuration()) * data.length,
          )
          const energyData = data[index]
          const energy = (Math.abs(energyData[0]) + Math.abs(energyData[1])) / 2
          if (energy > average) {
            const max = 1
            const progress = (1 - (max - energy) / (max - average)) / 2
            return Math.min(1, 0.5 + progress)
          } else if (energy < average) {
            const progress = (1 - (min - energy) / (min - average)) / 2
            return Math.max(0, 0.5 - progress)
          }
        } catch (e) {
          console.error(e)
        }
        return 0.5
      } else if (this.energyIntensity !== null) {
        return this.energyIntensity
      }
      return 0.5
    }
    setEnergyIntensity(value) {
      this.energyIntensity = value
    }
    spawnParticles({ delta, energy }) {
      this.emitters.forEach((emitter) => {
        const max = this.maxParticles - this.particles.length
        const particles = emitter.tick({ delta, energy, max })
        if (particles.length > 0) {
          particles.forEach((particle) => this.particles.push(particle))
        }
      })
    }
    moveParticles({ delta, energy }) {
      const currentParticles = []
      const leftBounds = -300
      const rightBounds = this.width + 100
      const topBounds = -100
      const bottomBounds = this.height + 100
      this.particles.forEach((particle) => {
        const x = particle.position.x
        const y = particle.position.y
        if (
          x > leftBounds &&
          x < rightBounds &&
          y > topBounds &&
          y < bottomBounds &&
          particle.timeLived < particle.lifetime
        ) {
          particle.move({ delta, energy })
          currentParticles.push(particle)
        }
      })
      this.particles = currentParticles
    }
    draw() {
      this.particles.forEach((particle) => particle.draw())
    }
    queue() {
      window.requestAnimationFrame(() => {
        this.tick()
      })
    }
    sum(arr) {
      return arr.reduce((acc, cur) => {
        return acc + cur
      }, 0)
    }
  }
  function createParticleSystem({
    background,
    foreground,
    data,
    player,
    waveData,
  }) {
    const defaultEmitter = {
      position: {
        type: 'vector',
        value: [0, 0],
      },
      speed: {
        type: 'float',
        value: 0,
      },
      angle: {
        type: 'float',
        value: 0,
      },
      rate: {
        type: 'float',
        value: 0,
      },
      layer: {
        type: 'string',
        value: 'foreground',
      },
    }
    const defaultParticle = {
      shape: {
        type: 'string',
        value: 'circle',
      },
      gravity: {
        type: 'float',
        value: 0,
      },
      drag: {
        type: 'vector',
        value: [0, 0],
      },
      bounce: {
        type: 'float',
        value: 0,
      },
      wobble: {
        type: 'float',
        value: 0,
      },
      scale: {
        type: 'float',
        value: 1,
      },
      scaleRate: {
        type: 'float',
        value: 0,
      },
      rotation: {
        type: 'float',
        value: 0,
      },
      rotationRate: {
        type: 'float',
        value: 0,
      },
      lifetime: {
        type: 'float',
        value: 30,
      },
      color: {
        type: 'color',
        value: [255, 255, 255, 1],
      },
      edgeBehaviour: {
        type: 'string',
        value: 'fade',
      },
      image: {
        type: 'string',
        value: '',
      },
      svg: {
        type: 'string',
        value: '',
      },
    }
    const defaultTrail = {
      shape: {
        type: 'string',
        value: 'square',
      },
      length: {
        type: 'float',
        value: 0,
      },
      color: {
        type: 'color',
        value: [255, 255, 255, 1],
      },
      size: {
        type: 'float',
        value: 1,
      },
    }
    const system = {
      waveformData: [],
      prerender: 0,
      emitters: [],
      background: background,
      foreground: foreground,
      player,
      waveData,
    }
    const bgCtx = background.getContext('2d')
    const fgCtx = foreground.getContext('2d')
    data?.emitters?.forEach((emitterData) => {
      const id = emitterData.id
      const emitter = Object.keys(defaultEmitter).reduce((acc, cur) => {
        const emitterConfig = emitterData?.[cur] || defaultEmitter[cur]
        acc[cur] = convertData(emitterConfig)
        return acc
      }, {})
      const particle = Object.keys(defaultParticle).reduce((acc, cur) => {
        const particleConfig =
          emitterData?.particle?.[cur] || defaultParticle[cur]
        acc[cur] = convertData(particleConfig)
        acc.trail = Object.keys(defaultTrail).reduce((trailAcc, trailCur) => {
          const trailConfig =
            emitterData?.particle?.trail?.[trailCur] || defaultTrail[trailCur]
          trailAcc[trailCur] = convertData(trailConfig)
          return trailAcc
        }, {})
        return acc
      }, {})
      const effects = data?.effects.reduce((acc, cur) => {
        if (cur?.emitters?.includes(id)) {
          acc.push(cur)
        }
        return acc
      }, [])
      system.emitters.push(
        new Emitter({
          ...emitter,
          particle,
          background,
          foreground,
          effects,
          ctx: emitter.layer === 'foreground' ? fgCtx : bgCtx,
          system,
        }),
      )
    })
    return new ParticleSystem(system)
  }
  function convertData(data) {
    let v = data.value
    let type = data.type
    if (type.indexOf('choose') !== -1) {
      return new Choose(data)
    }
    switch (type) {
      case 'float':
        return Number(v)
      case 'string':
        return String(v)
      case 'color':
        return new Color(v[0], v[1], v[2], v[3])
      case 'colorOverLife':
        return new ColorOverLife(v)
      case 'vector':
        return new Vector(v[0], v[1])
      case 'range':
        return new Range(Number(v[0]), Number(v[1]))
      case 'vectorRange':
        return new Range(
          new Vector(v[0][0], v[0][1]),
          new Vector(v[1][0], v[1][1]),
        )
    }
  }
  class Choose {
    constructor(data) {
      this.data = data
    }
    static render(input) {
      if (input instanceof Choose) {
        const data = input.data
        const tempType = data.type.replace('choose', '')
        const value = data.value[Math.floor(Math.random() * data.value.length)]
        const type = tempType.charAt(0).toLowerCase() + tempType.slice(1)
        return convertData({ type, value })
      }
      return input
    }
  }
  class Vector {
    constructor(x, y) {
      if (Array.isArray(x)) {
        this.x = x[0]
        this.y = x[1]
      } else if (typeof x === 'object') {
        this.x = x.x
        this.y = x.y
      } else {
        this.x = x || 0
        this.y = y === undefined ? x : y || 0
      }
    }
    get() {
      return { x: this.x, y: this.y }
    }
    add(vector) {
      return new Vector(this.x + vector.x, this.y + vector.y)
    }
    subtract(vector) {
      return new Vector(this.x - vector.x, this.y - vector.y)
    }
    multiply(input) {
      if (input instanceof Vector) {
        return new Vector(this.x * input.x, this.y * input.y)
      }
      return new Vector(this.x * input, this.y * input)
    }
    getMagnitude() {
      return Math.sqrt(this.x * this.x + this.y * this.y)
    }
    getAngle() {
      return Math.atan2(this.y, this.x)
    }
    dotProduct(vector) {
      return this.x * vector.x + this.y * vector.y
    }
    static fromAngle(angle, magnitude) {
      return new Vector(
        magnitude * Math.cos(angle),
        magnitude * Math.sin(angle),
      )
    }
  }
  class Range {
    constructor(start, end) {
      this.start = start
      this.end = end
    }
    static render(input) {
      if (input instanceof Range) {
        if (input.start instanceof Vector) {
          return new Vector(
            input.start.x + Math.random() * (input.end.x - input.start.x),
            input.start.y + Math.random() * (input.end.y - input.start.y),
          )
        }
        return input.start + Math.random() * (input.end - input.start)
      }
      return input
    }
  }
  class Color {
    constructor(r, g, b, a = 255) {
      this.red = r
      this.green = g
      this.blue = b
      this.alpha = a
    }
    rgba() {
      return `${this.red}, ${this.green}, ${this.blue}, ${this.alpha}`
    }
    render() {
      return this.rgba()
    }
    copy() {
      return new Color(this.red, this.green, this.blue, this.alpha)
    }
    getColor() {
      return this
    }
    isWhite() {
      return this.red === 255 && this.green === 255 && this.blue === 255
    }
  }
  class ColorOverLife {
    constructor(keyframes) {
      this.keyframes = keyframes
    }
    getColor(lifetimeRatio) {
      const normalizedLife = 100 * lifetimeRatio
      const currentFrameIndex =
        this.keyframes.findLastIndex(
          (frame) => frame.keyframe <= normalizedLife,
        ) || 0
      const currentFrame = this.keyframes[currentFrameIndex]
      const nextFrame =
        this.keyframes[currentFrameIndex + 1] ||
        this.keyframes[this.keyframes.length - 1]
      if (nextFrame.keyframe === currentFrame.keyframe) {
        return new Color(...currentFrame.value)
      }
      const progress =
        (normalizedLife - currentFrame.keyframe) /
        (nextFrame.keyframe - currentFrame.keyframe)
      let red = currentFrame.value[0],
        green = currentFrame.value[1],
        blue = currentFrame.value[2],
        alpha = currentFrame.value[3]
      const nextRed = nextFrame.value[0],
        nextGreen = nextFrame.value[1],
        nextBlue = nextFrame.value[2],
        nextAlpha = nextFrame.value[3]
      if (red !== nextRed) {
        red = red * (1 - progress) + nextRed * progress
      }
      if (green !== nextGreen) {
        green = green * (1 - progress) + nextGreen * progress
      }
      if (blue !== nextBlue) {
        blue = blue * (1 - progress) + nextBlue * progress
      }
      if (alpha !== nextAlpha) {
        alpha = alpha * (1 - progress) + nextAlpha * progress
      }
      return new Color(red, green, blue, alpha)
    }
    render(lifetimeRatio) {
      const color = this.getColor(lifetimeRatio)
      return color.render()
    }
  }
  function getEffectStrength(effect, baseAmount, energy) {
    const fullAmount = effect.data.amount
    if (effect.energy === 'all') {
      return fullAmount
    } else if (effect.energy === 'above' && energy >= ENERGY_HIGH_THRESHOLD) {
      const eStart = ENERGY_HIGH_THRESHOLD
      const eEnd = 1
      return (
        baseAmount +
        (effect.data.amount - baseAmount) *
          ((energy - eStart) / (eEnd - eStart))
      )
    } else if (effect.energy === 'below' && energy <= ENERGY_LOW_THRESHOLD) {
      const eStart = ENERGY_LOW_THRESHOLD
      const eEnd = 0
      return (
        baseAmount +
        (effect.data.amount - baseAmount) *
          ((energy - eStart) / (eEnd - eStart))
      )
    }
    return baseAmount
  }
  function getEffectStrengthVector(effect, baseAmount, energy) {
    const fullAmount = new Vector(effect.data.amount)
    if (effect.energy === 'all') {
      return fullAmount
    } else if (effect.energy === 'above' && energy >= ENERGY_HIGH_THRESHOLD) {
      const eStart = ENERGY_HIGH_THRESHOLD
      const eEnd = 1
      const modifier = (energy - eStart) / (eEnd - eStart)
      return new Vector(
        baseAmount.x + (fullAmount.x - baseAmount.x) * modifier,
        baseAmount.y + (fullAmount.y - baseAmount.y) * modifier,
      )
    } else if (effect.energy === 'below' && energy <= ENERGY_LOW_THRESHOLD) {
      const eStart = ENERGY_LOW_THRESHOLD
      const eEnd = 0
      const modifier = (energy - eStart) / (eEnd - eStart)
      return new Vector(
        baseAmount.x + (fullAmount.x - baseAmount.x) * modifier,
        baseAmount.y + (fullAmount.y - baseAmount.y) * modifier,
      )
    }
    return baseAmount
  }
})()