Full Code of katspaugh/wavesurfer.js for AI

main 13339e7dc699 cached
138 files
639.0 KB
171.4k tokens
505 symbols
1 requests
Download .txt
Showing preview only (676K chars total). Download the full file or copy to clipboard to get everything.
Repository: katspaugh/wavesurfer.js
Branch: main
Commit: 13339e7dc699
Files: 138
Total size: 639.0 KB

Directory structure:
gitextract_apdo23lc/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   └── question.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── build/
│       │   └── action.yml
│       ├── e2e.yml
│       ├── label-sponsors.yml
│       ├── lint.yml
│       ├── release.yml
│       ├── unit-tests.yml
│       └── yarn/
│           └── action.yml
├── .gitignore
├── .prettierrc
├── AGENTS.md
├── AI_OVERVIEW.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cypress/
│   ├── e2e/
│   │   ├── abort.cy.js
│   │   ├── basic.cy.js
│   │   ├── envelope.cy.js
│   │   ├── error.cy.js
│   │   ├── hover.cy.js
│   │   ├── index.html
│   │   ├── options.cy.js
│   │   ├── regions-no-audio.cy.js
│   │   ├── regions.cy.js
│   │   ├── spectrogram.cy.js
│   │   ├── umd.cy.js
│   │   ├── umd.html
│   │   └── webaudio.cy.js
│   └── support/
│       ├── commands.ts
│       └── e2e.ts
├── cypress.config.js
├── eslint.config.js
├── examples/
│   ├── _preview.js
│   ├── all-options.js
│   ├── audio/
│   │   └── reed.sh
│   ├── bars.js
│   ├── basic.js
│   ├── custom-render.js
│   ├── envelope.js
│   ├── events.js
│   ├── fm-synth.js
│   ├── gradient.js
│   ├── hover.js
│   ├── minimap.js
│   ├── multitrack.js
│   ├── phase-vocoder/
│   │   └── index.js
│   ├── pitch-worker.js
│   ├── pitch.js
│   ├── predecoded.js
│   ├── react-global-player.js
│   ├── react.js
│   ├── record-sync.js
│   ├── record.js
│   ├── regions.js
│   ├── silence.js
│   ├── soundcloud.js
│   ├── spectrogram-windowed.js
│   ├── spectrogram.js
│   ├── speed.js
│   ├── split-channels.js
│   ├── styling.js
│   ├── timeline-custom.js
│   ├── timeline.js
│   ├── video.js
│   ├── vowels.js
│   ├── webaudio-shim.js
│   ├── webaudio.js
│   ├── zoom-plugin.js
│   └── zoom.js
├── index.html
├── jest.config.js
├── package.json
├── rollup.config.js
├── scripts/
│   ├── clean.cjs
│   ├── plugin.sh
│   └── plugin.ts.template
├── src/
│   ├── __tests__/
│   │   ├── base-plugin.test.ts
│   │   ├── dom.test.ts
│   │   ├── draggable.test.ts
│   │   ├── event-emitter.test.ts
│   │   ├── fetcher.test.ts
│   │   ├── memory-leaks.test.ts
│   │   ├── minimap.test.ts
│   │   ├── player.test.ts
│   │   ├── regions.test.ts
│   │   ├── renderer-utils.test.ts
│   │   ├── renderer.test.ts
│   │   ├── timeline.test.ts
│   │   ├── timer.test.ts
│   │   └── wavesurfer.test.ts
│   ├── base-plugin.ts
│   ├── decoder.ts
│   ├── dom.ts
│   ├── draggable.ts
│   ├── event-emitter.ts
│   ├── fetcher.ts
│   ├── fft.ts
│   ├── player.ts
│   ├── plugins/
│   │   ├── envelope.ts
│   │   ├── hover.ts
│   │   ├── minimap.ts
│   │   ├── record.ts
│   │   ├── regions.ts
│   │   ├── spectrogram-windowed.ts
│   │   ├── spectrogram-worker.ts
│   │   ├── spectrogram.ts
│   │   ├── timeline.ts
│   │   └── zoom.ts
│   ├── reactive/
│   │   ├── README.md
│   │   ├── __tests__/
│   │   │   ├── drag-stream.test.ts
│   │   │   ├── event-stream-emitter.test.ts
│   │   │   ├── event-streams.test.ts
│   │   │   ├── media-event-bridge.test.ts
│   │   │   ├── render-scheduler.test.ts
│   │   │   ├── scroll-stream.test.ts
│   │   │   ├── state-event-emitter.test.ts
│   │   │   └── store.test.ts
│   │   ├── drag-stream.ts
│   │   ├── event-stream-emitter.ts
│   │   ├── event-streams.ts
│   │   ├── media-event-bridge.ts
│   │   ├── render-scheduler.ts
│   │   ├── scroll-stream.ts
│   │   ├── state-event-emitter.ts
│   │   └── store.ts
│   ├── renderer-utils.ts
│   ├── renderer.ts
│   ├── state/
│   │   ├── __tests__/
│   │   │   └── wavesurfer-state.test.ts
│   │   └── wavesurfer-state.ts
│   ├── timer.ts
│   ├── wavesurfer.ts
│   └── webaudio.ts
├── tsconfig.json
└── tsconfig.test.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# http://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .github/FUNDING.yml
================================================
github: [katspaugh]


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug report
about: Report a bug you found in wavesurfer.js
title: ''
labels: bug
assignees: ''

---

<!--
BEFORE SUBMITTING:
 * Please search in the existing issues to make sure this issue hasn't been reported already
 * If you're not 100% certain if it's a bug in wavesurfer or your own code, please DO NOT create an issue and ask in the Q&A first: https://github.com/katspaugh/wavesurfer.js/discussions/categories/q-a
 * The sections below are required to fill out. Bug reports without a minimal code snippet and other required information will be immediately closed.
-->

## Bug description


## Environment
 - Browser: Chrome

## Minimal code snippet


## Expected result


## Obtained result


## Screenshots


================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Have a question or facing a roadblock with wavesurfer?
title: 'DO NOT CREATE THIS ISSUE – 不要创建这个 issue!'
labels: question
assignees: ''
---

⚠️ READ CAREFULLY: ⚠️

Do NOT create an ISSUE if it's a question. Post it IN THE Q&A FORUM instead.

请改为创建一个问答讨论帖!

👉 https://github.com/katspaugh/wavesurfer.js/discussions/categories/q-a

Thank you.


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Short description
Resolves #

## Implementation details


## How to test it


## Screenshots


## Checklist
* [ ] This PR is covered by e2e tests
* [ ] It introduces no breaking API changes


================================================
FILE: .github/workflows/build/action.yml
================================================
name: 'Build'

description: 'Build the app'

runs:
  using: 'composite'
  steps:
    - name: Build
      shell: bash
      run: yarn build


================================================
FILE: .github/workflows/e2e.yml
================================================
name: e2e

on:
  pull_request:

jobs:
  e2e:
    runs-on: ubuntu-latest
    name: E2E tests
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/workflows/yarn

      - uses: browser-actions/setup-chrome@v1

      - name: Install Cypress
        run: |
          ./node_modules/.bin/cypress install

      - uses: ./.github/workflows/build

      - uses: cypress-io/github-action@v4
        with:
          spec: cypress/e2e/*.cy.js
          browser: chrome
          record: false

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots


================================================
FILE: .github/workflows/label-sponsors.yml
================================================
name: Label sponsors
on:
  pull_request:
    types: [opened]
  issues:
    types: [opened]
jobs:
  build:
    name: is-sponsor-label
    runs-on: ubuntu-latest
    steps:
      - uses: JasonEtco/is-sponsor-label-action@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [pull_request]

jobs:
  eslint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/workflows/yarn

      - name: Run lint
        run: npm run lint:report
        continue-on-error: true

      - name: Annotate code with linting results
        uses: ataylorme/eslint-annotate-action@v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          report-json: "eslint_report.json"


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  publish-npm:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Extract version
        id: version
        run: |
          OLD_VERSION=$(npm show . version)
          NEW_VERSION=$(node -p 'require("./package.json").version')
          if [ $NEW_VERSION != $OLD_VERSION ]; then
            echo "New version $NEW_VERSION detected"
            echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
            git log "$OLD_VERSION"..HEAD --pretty=format:"* %s by %ae" | sed -E 's/by [0-9]+\+(.+)@users.noreply.github.com/by @\1/g' | sed -E 's/ by @?katspaugh.*//g' > TEMP_CHANGELOG.md
            echo -e "\n\n---\n[![npm](https://img.shields.io/npm/v/wavesurfer.js)](https://www.npmjs.com/package/wavesurfer.js/v/$NEW_VERSION)" >> TEMP_CHANGELOG.md
          else
            echo "Version $OLD_VERSION hasn't changed, skipping the release"
          fi

      - name: Create a git tag
        if: ${{ steps.version.outputs.version }}
        run: git tag $NEW_VERSION && git push --tags
        env:
          NEW_VERSION: ${{ steps.version.outputs.version }}

      - name: GitHub release
        if: ${{ steps.version.outputs.version }}
        uses: softprops/action-gh-release@v1
        id: create_release
        with:
          draft: false
          prerelease: false
          name: ${{ steps.version.outputs.version }}
          tag_name: ${{ steps.version.outputs.version }}
          body_path: TEMP_CHANGELOG.md
        env:
          GITHUB_TOKEN: ${{ github.token }}

      - uses: actions/setup-node@v3
        if: ${{ steps.version.outputs.version }}
        with:
          node-version: '16.x'
          registry-url: 'https://registry.npmjs.org'

      - uses: ./.github/workflows/yarn
        if: ${{ steps.version.outputs.version }}

      - name: Publish to NPM
        if: ${{ steps.version.outputs.version }}
        env:
          NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
        run: npm publish


================================================
FILE: .github/workflows/unit-tests.yml
================================================
name: Unit Tests
on: [pull_request]

jobs:
  jest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/workflows/yarn
      - name: Run unit tests
        run: yarn test:unit


================================================
FILE: .github/workflows/yarn/action.yml
================================================
name: 'Yarn'

description: 'Install node modules'

runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v3.6.0
      with:
        node-version: 20

    - name: Yarn cache
      uses: actions/cache@v3
      with:
        path: '**/node_modules'
        key: node-modules-${{ hashFiles('**/yarn.lock') }}

    - name: Yarn install
      shell: bash
      run: yarn install --immutable


================================================
FILE: .gitignore
================================================
.DS_Store
dist
docs
node_modules
yarn-error.log
cypress/screenshots
cypress/downloads
cypress/videos
cypress/**/__diff_output__/
.env
coverage/
eslint_report.json


================================================
FILE: .prettierrc
================================================
{
  "tabWidth": 2,
  "printWidth": 120,
  "trailingComma": "all",
  "singleQuote": true,
  "semi": false,
  "endOfLine": "auto"
}


================================================
FILE: AGENTS.md
================================================
# Coding Conventions

- Use TypeScript. Prefer ES modules.
- Follow the repo Prettier configuration (2 spaces, print width 120, single quotes, no semicolons, trailing commas).
- Do not commit files from `dist` or `node_modules`.

# Programmatic Checks

1. `yarn lint`

Run these after making changes. If a command fails due to environment limits, note this in the PR.

# Pull Request Guidelines

When opening a PR, use the provided template and include:
- **Short description**
- **Implementation details**
- **How to test it**
- **Checklist** with the items from `.github/PULL_REQUEST_TEMPLATE.md`.

The title of the PR should follow the semantic commit convention (e.g. `fix(Regions): remove unused variable`).


================================================
FILE: AI_OVERVIEW.md
================================================
# Repository Overview for AI Agents

This document gives a condensed view of the project structure and build process so that AI tools (like Codex) can reason about the codebase without scanning every file.

## Project Structure

- **`src/`** – TypeScript source files for the library. The entry point is [`wavesurfer.ts`](../src/wavesurfer.ts). Other files implement features such as the player, plugins, and utilities.
- **`examples/`** – Stand‑alone demos used for manual testing and documentation. Each example is an HTML page importing the library and demonstrating a specific feature.
- **`cypress/`** – End‑to‑end and visual regression tests powered by Cypress. Tests live in `cypress/e2e` and snapshots reside in `cypress/snapshots`.
- **`scripts/`** – Helper scripts for cleaning the build directory and creating new plugins.
- **Root config files** – `package.json` defines the build, lint, and test commands. TypeScript configuration is in `tsconfig.json`, and linting rules are in `.eslintrc` and `.prettierrc`.

## Common Tasks

- **Install dependencies**: `yarn`
- **Run the dev server**: `yarn start` (compiles TypeScript in watch mode and serves examples on <http://localhost:9090>)
- **Build for production**: `yarn build`
- **Run lint checks**: `yarn lint`
- **Run Cypress tests**: `yarn cypress`

## Contribution Notes

- Follow the coding conventions in [`AGENTS.md`](AGENTS.md).
- Do not commit generated files from `dist/` or `node_modules/`.

This overview should help an AI agent quickly locate relevant source files and scripts without traversing the entire repository.


================================================
FILE: CONTRIBUTING.md
================================================
# CONTRIBUTING to wavesurfer.js

Hello there,

Firstly, a heartfelt thank you! We sincerely appreciate your interest in wavesurfer.js and are really excited to see your contributions to our community.

Here are a few guidelines to keep in mind when you're ready to contribute:

## 1. Search in Existing Issues

Before submitting a new issue, we kindly ask you to take a moment to search through our [existing issues](https://github.com/katspaugh/wavesurfer.js/issues?q=is%3Aissue). There's a chance that someone has already raised the point you're interested in. This step helps to keep our issues page clean and productive.

## 2. Questions and Feature Requests

Got a burning question or a brilliant feature idea? That's fantastic! But instead of the issues section, we ask you to post these in our [Discussions](https://github.com/katspaugh/wavesurfer.js/discussions/categories/ideas) forum. This helps to separate enhancement ideas and questions from the bugs and issues which need immediate attention from the developers.

To visit the forum, [click here](https://github.com/katspaugh/wavesurfer.js/discussions).

## 3. Reporting Bugs

Stumbled upon a bug? Sorry about that! We're constantly working to improve wavesurfer.js and your bug reports help us do just that.

When you post a bug report, please include the necessary code that will help us reproduce the bug. The more details you provide, the quicker we can get to the root of the problem and resolve it.

By following these guidelines, you're helping us maintain a productive, organized community. We can't wait to see your contributions to wavesurfer.js. Thank you again for your help!


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2012-2023, katspaugh and contributors
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# <img src="https://user-images.githubusercontent.com/381895/226091100-f5567a28-7736-4d37-8f84-e08f297b7e1a.png" alt="logo" height="60" valign="middle" /> wavesurfer.js

[![npm](https://img.shields.io/npm/v/wavesurfer.js)](https://www.npmjs.com/package/wavesurfer.js) [![sponsor](https://img.shields.io/badge/sponsor_us-🤍-%23B14586)](https://github.com/sponsors/katspaugh)

**Wavesurfer.js** is an interactive waveform rendering and audio playback library, perfect for web applications. It leverages modern web technologies to provide a robust and visually engaging audio experience.

<img width="626" alt="waveform screenshot" src="https://github.com/katspaugh/wavesurfer.js/assets/381895/05f03bed-800e-4fa1-b09a-82a39a1c62ce">

**Gold sponsor 💖** [Closed Caption Creator](https://www.closedcaptioncreator.com)

# Table of contents

1. [Getting started](#getting-started)
2. [API reference](#api-reference)
3. [Plugins](#plugins)
4. [CSS styling](#css-styling)
5. [Frequent questions](#questions)
6. [Development](#development)
7. [Tests](#tests)
8. [Feedback](#feedback)

## Getting started

Install and import the package:

```bash
npm install --save wavesurfer.js
```
```js
import WaveSurfer from 'wavesurfer.js'
```

Alternatively, insert a UMD script tag which exports the library as a global `WaveSurfer` variable:
```html
<script src="https://unpkg.com/wavesurfer.js@7"></script>
```

Create a wavesurfer instance and pass various [options](http://wavesurfer.xyz/docs/options):
```js
const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: '#4F4A85',
  progressColor: '#383351',
  url: '/audio.mp3',
})
```

To import one of the plugins, e.g. the [Regions plugin](https://wavesurfer.xyz/examples/?regions.js):
```js
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js'
```

Or as a script tag that will export `WaveSurfer.Regions`:
```html
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
```

TypeScript types are included in the package, so there's no need to install `@types/wavesurfer.js`.

See more [examples](https://wavesurfer.xyz/examples).

## API reference

See the wavesurfer.js documentation on our website:

 * [methods](https://wavesurfer.xyz/docs/methods)
 * [options](http://wavesurfer.xyz/docs/options)
 * [events](http://wavesurfer.xyz/docs/events)

## Plugins

We maintain a number of official plugins that add various extra features:

 * [Regions](https://wavesurfer.xyz/examples/?regions.js) – visual overlays and markers for regions of audio
 * [Timeline](https://wavesurfer.xyz/examples/?timeline.js) – displays notches and time labels below the waveform
 * [Minimap](https://wavesurfer.xyz/examples/?minimap.js) – a small waveform that serves as a scrollbar for the main waveform
 * [Envelope](https://wavesurfer.xyz/examples/?envelope.js) – a graphical interface to add fade-in and -out effects and control volume
 * [Record](https://wavesurfer.xyz/examples/?record.js) – records audio from the microphone and renders a waveform
 * [Spectrogram](https://wavesurfer.xyz/examples/?spectrogram.js) – visualization of an audio frequency spectrum (written by @akreal)
 * [Hover](https://wavesurfer.xyz/examples/?hover.js) – shows a vertical line and timestmap on waveform hover

## CSS styling

wavesurfer.js v7 is rendered into a Shadow DOM tree. This isolates its CSS from the rest of the web page.
However, it's still possible to style various wavesurfer.js elements with CSS via the `::part()` pseudo-selector.
For example:

```css
#waveform ::part(cursor):before {
  content: '🏄';
}
#waveform ::part(region) {
  font-family: fantasy;
}
```

You can see which elements you can style in the DOM inspector – they will have a `part` attribute.
See [this example](https://wavesurfer.xyz/examples/?styling.js) to play around with styling.

## Questions

Have a question about integrating wavesurfer.js on your website? Feel free to ask in our [Discussions forum](https://github.com/wavesurfer-js/wavesurfer.js/discussions/categories/q-a).

However, please keep in mind that this forum is dedicated to wavesurfer-specific questions. If you're new to JavaScript and need help with the general basics like importing NPM modules, please consider asking ChatGPT or StackOverflow first.

### FAQ

<details>
  <summary>I'm having CORS issues</summary>
  Wavesurfer fetches audio from the URL you specify in order to decode it. Make sure this URL allows fetching data from your domain. In browser JavaScript, you can only fetch data eithetr from <b>the same domain</b> or another domain if and only if that domain enables <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a>. So if your audio file is on an external domain, make sure that domain sends the right Access-Control-Allow-Origin headers. There's nothing you can do about it from the requesting side (i.e. your JS code).
</details>

<details>
  <summary>Does wavesurfer support large files?</summary>
  Since wavesurfer decodes audio entirely in the browser using Web Audio, large clips may fail to decode due to memory constraints. We recommend using pre-decoded peaks for large files (see <a href="https://wavesurfer.xyz/examples/?predecoded.js">this example</a>). You can use a tool like <a href="https://codeberg.org/chrisn/audiowaveform">audiowaveform</a> to generate peaks.
</details>

<details>
  <summary>What about streaming audio?</summary>
  Streaming audio is supported only with <a href="https://wavesurfer.xyz/examples/?predecoded.js">pre-decoded peaks and duration</a>.
</details>

<details>
  <summary>There is a mismatch between my audio and the waveform. How do I fix it?</summary>
  If you're using a VBR (variable bit rate) audio file, there might be a mismatch between the audio and the waveform. This can be fixed by converting your file to CBR (constant bit rate).
  <p>Alternatively, you can use the <a href="https://wavesurfer.xyz/examples/?webaudio-shim.js">Web Audio shim</a> which is more accurate.</p>
</details>

<details>
  <summary>How do I connect wavesurfer.js to Web Audio effects?</summary>
Generally, wavesurfer.js doesn't aim to be a wrapper for all things Web Audio. It's just a player with a waveform visualization. It does allow connecting itself to a Web Audio graph by exporting its audio element (see <a href="https://wavesurfer.xyz/examples/?4436ec40a2ab943243755e659ae32196">this example</a>) but nothign more than that. Please don't expect wavesurfer to be able to cut, add effects, or process your audio in any way.
</details>

## Development

To get started with development, follow these steps:

 1. Install dev dependencies:

```
yarn
```

 2. Start the TypeScript compiler in watch mode and launch an HTTP server:

```
yarn start
```

This command will open http://localhost:9090 in your browser with live reload, allowing you to see the changes as you develop.

## Tests

The tests are written in the Cypress framework. They are a mix of e2e and visual regression tests.

To run the test suite locally, first build the project:
```
yarn build
```

Then launch the tests:
```
yarn cypress
```

## Feedback

We appreciate your feedback and contributions!

If you encounter any issues or have suggestions for improvements, please don't hesitate to post in our [forum](https://github.com/wavesurfer-js/wavesurfer.js/discussions/categories/q-a).

We hope you enjoy using wavesurfer.js and look forward to hearing about your experiences with the library!


================================================
FILE: cypress/e2e/abort.cy.js
================================================
describe('WaveSurfer abort handling tests', () => {
  beforeEach(() => {
    cy.visit('cypress/e2e/index.html')

    cy.window().its('WaveSurfer').should('exist')
  })

  // https://github.com/katspaugh/wavesurfer.js/issues/3637
  it('load url after destroyed should emit ready', () => {
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
        })

        win.wavesurfer.destroy()

        win.wavesurfer.load('../../examples/audio/demo.wav')

        win.wavesurfer.on('ready', resolve)
      })
    })
  })

  it('destroy before wavesurfer ready should throw AbortError Exception', () => {
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
        })

        // catch load error
        win.wavesurfer.load('../../examples/audio/demo.wav').catch((e) => {
          expect(e.name).to.equal('AbortError')
          expect(e.message).to.match(/aborted/)
          resolve()
        })

        win.wavesurfer.destroy()
      })
    })
  })

  it('destroy before wavesurfer ready should emit AbortError Exception', () => {
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
        })

        win.wavesurfer.load('../../examples/audio/demo.wav').catch(() => {})

        win.wavesurfer.destroy()

        // listening wavesurfer emit error event
        win.wavesurfer.on('error', (e) => {
          expect(e.name).to.equal('AbortError')
          expect(e.message).to.match(/aborted/)
          resolve()
        })
      })
    })
  })
})


================================================
FILE: cypress/e2e/basic.cy.js
================================================
describe('WaveSurfer basic tests', () => {
  beforeEach((done) => {
    cy.visit('cypress/e2e/index.html')

    cy.window().its('WaveSurfer').should('exist')

    cy.window().then((win) => {
      const waitForReady = new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
          url: '../../examples/audio/demo.wav',
        })

        win.wavesurfer.once('ready', () => resolve())
      })

      cy.wrap(waitForReady).then(done)
    })
  })

  it('should instantiate WaveSurfer without errors', () => {
    cy.window().its('wavesurfer').should('be.an', 'object')
  })

  it('should emit a redrawcomplete event', () => {
    cy.window().then((win) => {
      const { wavesurfer } = win
      expect(wavesurfer.getDuration().toFixed(2)).to.equal('21.77')

      wavesurfer.options.minPxPerSec = 200
      wavesurfer.load('../../examples/audio/audio.wav')

      return new Promise((resolve) => {
        wavesurfer.once('redrawcomplete', () => {
          wavesurfer.zoom(100)
          wavesurfer.once('redrawcomplete', () => {
            resolve()
          })
        })
      })
    })
  })

  it('should load an audio file without errors', () => {
    cy.window().then((win) => {
      expect(win.wavesurfer.getDuration().toFixed(2)).to.equal('21.77')

      win.wavesurfer.load('../../examples/audio/audio.wav')

      return new Promise((resolve) => {
        win.wavesurfer.once('ready', () => {
          expect(win.wavesurfer.getDuration().toFixed(2)).to.equal('26.39')
          resolve()
        })
      })
    })
  })

  it('should catch fetch errors', () => {
    cy.window().then((win) => {
      return win.wavesurfer.load('../../examples/audio/audio.w1av').catch((e) => {
        expect(e.message).to.equal('Failed to fetch ../../examples/audio/audio.w1av: 404 (Not Found)')
      })
    })
  })

  it('should play and pause audio', () => {
    cy.window().then((win) => {
      expect(win.wavesurfer.getCurrentTime()).to.equal(0)

      win.wavesurfer.play()

      cy.wait(1000).then(() => {
        expect(win.wavesurfer.isPlaying()).to.be.true

        win.wavesurfer.pause()

        expect(win.wavesurfer.getCurrentTime()).to.be.greaterThan(0)
      })
    })
  })

  it('should set and get volume without errors', () => {
    cy.window().then((win) => {
      win.wavesurfer.setVolume(0.5)
      expect(win.wavesurfer.getVolume()).to.equal(0.5)
    })
  })

  it('should set and get muted state without errors', () => {
    cy.window().then((win) => {
      win.wavesurfer.setMuted(true)
      expect(win.wavesurfer.getMuted()).to.be.true
    })
  })

  it('should set and get playback rate without errors', () => {
    cy.window().then((win) => {
      win.wavesurfer.setPlaybackRate(1.5)
      expect(win.wavesurfer.getPlaybackRate()).to.equal(1.5)
    })
  })

  it('should seek to a time in seconds', () => {
    cy.window().then((win) => {
      win.wavesurfer.setTime(10.1)
      expect(win.wavesurfer.getCurrentTime()).to.equal(10.1)
      expect(win.wavesurfer.getScroll()).to.equal(0) // no scroll
    })
  })

  it('should set the zoom level', () => {
    cy.window().then((win) => {
      const initialWidth = win.wavesurfer.getWrapper().clientWidth

      win.wavesurfer.zoom(200)
      const zoomedWidth = win.wavesurfer.renderer.getWrapper().clientWidth
      expect(zoomedWidth).to.be.greaterThan(initialWidth)
      win.wavesurfer.zoom(600)
      const newWidth = win.wavesurfer.getWrapper().clientWidth

      expect(Math.round(newWidth / zoomedWidth)).to.equal(3)
    })
  })

  it('should scroll on seek if zoomed in', () => {
    cy.window().then((win) => {
      win.wavesurfer.zoom(300)
      const zoomedWidth = win.wavesurfer.getWrapper().clientWidth
      win.wavesurfer.zoom(600)
      const newWidth = win.wavesurfer.getWrapper().clientWidth

      expect(Math.round(newWidth / zoomedWidth)).to.equal(2)

      win.wavesurfer.setTime(20)

      cy.wait(1000).then(() => {
        expect(win.wavesurfer.getScroll()).to.be.greaterThan(100)
      })
    })
  })

  it('should scroll on setScrollTime if zoomed in', () => {
    cy.window().then((win) => {
      win.wavesurfer.zoom(300)
      const zoomedWidth = win.wavesurfer.getWrapper().clientWidth
      win.wavesurfer.zoom(600)
      const newWidth = win.wavesurfer.getWrapper().clientWidth

      expect(Math.round(newWidth / zoomedWidth)).to.equal(2)

      win.wavesurfer.setScrollTime(20)

      cy.wait(1000).then(() => {
        expect(win.wavesurfer.getScroll()).to.be.greaterThan(100)
      })
    })
  })

  it('should export decoded audio data', () => {
    cy.window().then((win) => {
      const data = win.wavesurfer.getDecodedData()

      expect(data.getChannelData).to.be.a('function')
      expect(data.length).to.equal(174191)
      expect(data.sampleRate).to.equal(8000)
      expect(data.duration.toFixed(2)).to.equal('21.77')
    })
  })

  it('should not fill the container if fillParent is false', () => {
    cy.window().then((win) => {
      win.wavesurfer.setOptions({
        fillParent: false,
        minPxPerSec: 10,
      })
      expect(win.document.querySelector('#waveform').clientWidth).to.greaterThan(
        win.wavesurfer.getWrapper().clientWidth,
      )
    })
  })

  it('should export peaks', () => {
    cy.window().then((win) => {
      const peaks = win.wavesurfer.exportPeaks({
        channels: 2,
        maxLength: 1000,
        precision: 100,
      })
      expect(peaks.length).to.equal(1) // the file is mono
      expect(peaks[0].length).to.equal(1000)
      expect(peaks[0][0]).to.equal(0.01)
      expect(peaks[0][99]).to.equal(0.3)
      expect(peaks[0][100]).to.equal(0.31)

      const peaksB = win.wavesurfer.exportPeaks({
        maxLength: 1000,
        precision: 1000,
      })
      expect(peaksB.length).to.equal(1)
      expect(peaksB[0].length).to.equal(1000)
      expect(peaksB[0][0]).to.equal(0.015)
      expect(peaksB[0][99]).to.equal(0.296)
      expect(peaksB[0][100]).to.equal(0.308)

      const peaksC = win.wavesurfer.exportPeaks()
      expect(peaksC.length).to.equal(1)
      expect(peaksC[0].length).to.equal(8000)
      expect(peaksC[0][0]).to.equal(0.0117)
      expect(peaksC[0][99]).to.equal(0.0076)
      expect(peaksC[0][100]).to.equal(0.01)
    })
  })

  describe('exportImage', () => {
    it('should export an image as a data-URI', () => {
      cy.window()
        .then((win) => {
          return win.wavesurfer.exportImage()
        })
        .then((data) => {
          expect(data[0]).to.match(/^data:image\/png;base64,/)
        })
    })

    it('should export an image as a JPEG data-URI', () => {
      cy.window()
        .then((win) => {
          return win.wavesurfer.exportImage('image/jpeg', 0.75)
        })
        .then((data) => {
          expect(data[0]).to.match(/^data:image\/jpeg;base64,/)
        })
    })

    it('should export an image as a blob', () => {
      cy.window()
        .then((win) => {
          return win.wavesurfer.exportImage('image/webp', 0.75, 'blob')
        })
        .then((data) => {
          expect(data[0]).to.be.a('blob')
        })
    })
  })

  it('should destroy wavesurfer', () => {
    cy.window().then((win) => {
      win.wavesurfer.destroy()
    })
  })

  describe('setMediaElement', () => {
    // Mock add/remove event listeners for `media` elements
    const attachMockListeners = (el) => {
      el.eventCount = 0

      const addEventListener = el.addEventListener
      el.addEventListener = (eventName, callback, options) => {
        if (!options || !options.once) el.eventCount++
        addEventListener.call(el, eventName, callback, options)
      }
      const removeEventListener = el.removeEventListener
      el.removeEventListener = (eventName, callback) => {
        el.eventCount--
        removeEventListener.call(el, eventName, callback)
      }
    }

    beforeEach((done) => {
      cy.window().then((win) => {
        win.wavesurfer.destroy()

        const originalMedia = document.createElement('audio')
        attachMockListeners(originalMedia)

        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          url: '../../examples/audio/demo.wav',
          media: originalMedia,
        })

        win.wavesurfer.once('ready', () => done())
      })
    })

    it('should set media without errors', () => {
      cy.window().then((win) => {
        const media = document.createElement('audio')
        media.id = 'new-media'
        win.wavesurfer.setMediaElement(media)
        expect(win.wavesurfer.getMediaElement().id).to.equal('new-media')
      })
    })

    it('should unsubscribe events from removed media element', () => {
      cy.window().then((win) => {
        const originalMedia = win.wavesurfer.getMediaElement()
        const media = document.createElement('audio')

        expect(originalMedia.eventCount).to.be.greaterThan(0)

        win.wavesurfer.setMediaElement(media)
        expect(originalMedia.eventCount).to.be.lessThan(1)
      })
    })

    it('should subscribe events for newly set media element', () => {
      cy.window().then((win) => {
        const newMedia = document.createElement('audio')
        attachMockListeners(newMedia)

        win.wavesurfer.setMediaElement(newMedia)
        expect(newMedia.eventCount).to.be.greaterThan(0)
      })
    })
  })

  it('should return true when calling isPlaying() after play()', (done) => {
    cy.window().then((win) => {
      expect(win.wavesurfer.isPlaying()).to.be.false
      win.wavesurfer.play()
      expect(win.wavesurfer.isPlaying()).to.be.true
      win.wavesurfer.once('play', () => {
        expect(win.wavesurfer.isPlaying()).to.be.true
        win.wavesurfer.pause()
        expect(win.wavesurfer.isPlaying()).to.be.false
        done()
      })
    })
  })
})


================================================
FILE: cypress/e2e/envelope.cy.js
================================================
const id = '#waveform'

describe('WaveSurfer Envelope plugin tests', () => {
  it('should render an envelope', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 200,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Envelope.create({
              volume: 0.8,
              lineColor: 'rgba(255, 0, 0, 0.5)',
              lineWidth: 4,
              dragPointSize: 20,
              dragLine: false,
              dragPointFill: 'rgba(0, 255, 255, 0.8)',
              dragPointStroke: 'rgba(0, 0, 0, 0.5)',

              points: [
                { time: 11.2, volume: 0.5 },
                { time: 15.5, volume: 0.8 },
              ],
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          cy.get(id).matchImageSnapshot('envelope-basic')
          resolve()
        })
      })
    })
  })

  it('should render an envelope and add a point', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        const envelopePlugin = win.Envelope.create({
          volume: 0.5,
          lineColor: 'rgba(255, 0, 0, 0.5)',
          lineWidth: 10,
          dragPointSize: 12,
          dragLine: true,
          dragPointFill: 'rgba(0, 255, 255, 0.8)',
          dragPointStroke: 'rgba(0, 0, 0, 0.5)',

          points: [
            { time: 12.2, volume: 0.4 },
            { time: 16.5, volume: 0.9 },
          ],
        })

        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 200,
          url: '../../examples/audio/demo.wav',
          plugins: [envelopePlugin],
        })

        envelopePlugin.addPoint({ id: 'new-point', time: 10.1, volume: 0.6 })

        win.wavesurfer.once('ready', () => {
          cy.get(id).matchImageSnapshot('envelope-add-point')
          resolve()
        })
      })
    })
  })
})


================================================
FILE: cypress/e2e/error.cy.js
================================================
describe('WaveSurfer error handling tests', () => {
  it('should fire error event if provided file url does not exist', () => {
    cy.visit('cypress/e2e/index.html')

    cy.window().its('WaveSurfer').should('exist')

    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
          url: '../../examples/audio/DOES_NOT_EXIST.wav',
        })

        win.wavesurfer.on('error', () => {
          console.log('error event fired')
          resolve()
        })
      })
    })
  })
})


================================================
FILE: cypress/e2e/hover.cy.js
================================================
const id = '#waveform'

describe('WaveSurfer Hover plugin tests', () => {
  it('should render a label to the right with labelPreferLeft=false', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 500,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Hover.create({
              labelSize: '72px',
              labelPreferLeft: false,
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          // Move the mouse to the center of the container
          cy.get(id).trigger('pointermove', 'center')

          // Verify that the label got drawn on the right
          cy.wait(100)
          cy.get(id).matchImageSnapshot('hover-prefer-left-false')
          resolve()
        })
      })
    })
  })

  it('should render a label to the left with labelPreferLeft=false when near to the right edge', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 500,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Hover.create({
              labelSize: '72px',
              labelPreferLeft: false,
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          // Move the mouse to the right of the container
          cy.get(id).trigger('pointermove', 'right')

          // Verify that the label got drawn on the left
          cy.wait(100)
          cy.get(id).matchImageSnapshot('hover-prefer-left-false-near-right-edge')
          resolve()
        })
      })
    })
  })

  it('should render a label to the left with labelPreferLeft=true', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 500,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Hover.create({
              labelSize: '72px',
              labelPreferLeft: true,
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          // Move the mouse to the center of the container
          cy.get(id).trigger('pointermove', 'center')

          // Verify that the label got drawn on the left
          cy.wait(100)
          cy.get(id).matchImageSnapshot('hover-prefer-left-true')
          resolve()
        })
      })
    })
  })

  it('should render a label to the right with labelPreferLeft=true when near to the left edge', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 500,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Hover.create({
              labelSize: '72px',
              labelPreferLeft: true,
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          // Move the mouse to the center of the container
          cy.get(id).trigger('pointermove', 'left')

          // Verify that the label got drawn on the right
          cy.wait(100)
          cy.get(id).matchImageSnapshot('hover-prefer-left-true-near-left-edge')
          resolve()
        })
      })
    })
  })
})


================================================
FILE: cypress/e2e/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WaveSurfer Test</title>
  </head>
  <body>
    <div id="waveform" style="width: 600px"></div>
    <div id="otherWaveform" style="width: 600px"></div>

    <script type="module">
      import WaveSurfer from '../../dist/wavesurfer.js'
      import Regions from '../../dist/plugins/regions.js'
      import Timeline from '../../dist/plugins/timeline.js'
      import Spectrogram from '../../dist/plugins/spectrogram.js'
      import Envelope from '../../dist/plugins/envelope.js'
      import Hover from '../../dist/plugins/hover.js'

      window.WaveSurfer = WaveSurfer
      window.Regions = Regions
      window.Timeline = Timeline
      window.Spectrogram = Spectrogram
      window.Envelope = Envelope
      window.Hover = Hover
    </script>
  </body>
</html>


================================================
FILE: cypress/e2e/options.cy.js
================================================
const id = '#waveform'
const otherId = '#otherWaveform'

const wrapReady = (wavesurfer, event = 'ready') => {
  const waitForReady = new Promise((resolve) => {
    wavesurfer.once(event, resolve)
  })
  return cy.wrap(waitForReady)
}

describe('WaveSurfer options tests', () => {
  beforeEach(() => {
    cy.visit('cypress/e2e/index.html')
    cy.window().its('WaveSurfer').should('exist')
  })

  it('should use minPxPerSec and hideScrollbar', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        minPxPerSec: 100,
        hideScrollbar: true,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('minPxPerSec-hideScrollbar')
        done()
      })
    })
  })

  it('should use barWidth', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barWidth: 3,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('barWidth')
        done()
      })
    })
  })

  it('should use all bar options', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barWidth: 4,
        barGap: 3,
        barRadius: 4,
      })

      wavesurfer.once('ready', () => {
        cy.get(id).matchImageSnapshot('bars')
        done()
      })
    })
  })

  it('should use barAlign=top to align the waveform vertically', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barAlign: 'top',
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('barAlign-top')
        done()
      })
    })
  })

  it('should use barAlign=bottom to align the waveform vertically', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barAlign: 'bottom',
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('barAlign-bottom')
        done()
      })
    })
  })

  it('should use barAlign and barWidth together', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barAlign: 'bottom',
        barWidth: 4,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('barAlign-barWidth')
        done()
      })
    })
  })

  it('should use barHeight to scale the waveform vertically', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barHeight: 2,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('barHeight')
        done()
      })
    })
  })

  it('should use color options', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        waveColor: 'red',
        progressColor: 'green',
        cursorColor: 'blue',
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(10)
        cy.wait(100)
        cy.get(id).matchImageSnapshot('colors')
        done()
      })
    })
  })

  it('should use gradient color options', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        waveColor: ['rgb(200, 165, 49)', 'rgb(211, 194, 138)', 'rgb(205, 124, 49)', 'rgb(205, 98, 49)'],
        progressColor: 'rgba(0, 0, 0, 0.25)',
        cursorColor: 'blue',
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(10)
        cy.wait(100)
        cy.snap
        cy.get(id).matchImageSnapshot('colors-gradient')
        done()
      })
    })
  })

  it('should use cursor options', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        cursorColor: 'red',
        cursorWidth: 4,
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(10)
        cy.wait(100)
        cy.get(id).matchImageSnapshot('cursor')
        done()
      })
    })
  })

  it('should not scroll with autoScroll false', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        autoScroll: false,
        minPxPerSec: 200,
        hideScrollbar: true,
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(10)
        cy.wait(100)
        cy.get(id).matchImageSnapshot('autoScroll-false')
        done()
      })
    })
  })

  it('should not scroll to center with autoCenter false', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        autoCenter: false,
        minPxPerSec: 200,
        hideScrollbar: true,
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(10)
        cy.wait(100)
        cy.get(id).matchImageSnapshot('autoCenter-false')
        done()
      })
    })
  })

  it('should use peaks', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        peaks: [
          [
            0, 0.0023595101665705442, 0.012107174843549728, 0.005919494666159153, -0.31324470043182373,
            0.1511787623167038, 0.2473851442337036, 0.11443428695201874, -0.036057762801647186, -0.0968964695930481,
            -0.03033737652003765, 0.10682467371225357, 0.23974689841270447, 0.013210971839725971, -0.12377244979143143,
            0.046145666390657425, -0.015757400542497635, 0.10884027928113937, 0.06681904196739197, 0.09432944655418396,
            -0.17105795443058014, -0.023439358919858932, -0.10380347073078156, 0.0034454423002898693,
            0.08061369508504868, 0.026129156351089478, 0.18730352818965912, 0.020447958260774612, -0.15030759572982788,
            0.05689578503370285, -0.0009095853311009705, 0.2749626338481903, 0.2565386891365051, 0.07571295648813248,
            0.10791446268558502, -0.06575305759906769, 0.15336275100708008, 0.07056761533021927, 0.03287476301193237,
            -0.09044631570577621, 0.01777501218020916, -0.04906218498945236, -0.04756792634725571,
            -0.006875281687825918, 0.04520256072282791, -0.02362387254834175, -0.0668797641992569, 0.12266506254673004,
            -0.10895221680402756, 0.03791835159063339, -0.0195105392485857, -0.031097881495952606, 0.04252675920724869,
            -0.09187793731689453, 0.0829525887966156, -0.003812957089394331, 0.0431736595928669, 0.07634212076663971,
            -0.05335947126150131, 0.0345163568854332, -0.049201950430870056, 0.02300390601158142, 0.007677287794649601,
            0.015354577451944351, 0.007677287794649601, 0.007677288725972176,
          ],
        ],
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('peaks')
        done()
      })
    })
  })

  it('should use external media', (done) => {
    cy.window().then((win) => {
      const audio = new Audio('../../examples/audio/demo.wav')

      const wavesurfer = win.WaveSurfer.create({
        container: id,
        media: audio,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('media')
        done()
      })
    })
  })

  it('should split channels', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/stereo.mp3',
        splitChannels: true,
        waveColor: 'rgb(200, 0, 200)',
        progressColor: 'rgb(100, 0, 100)',
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setTime(2)
        cy.wait(100)
        cy.get(id).matchImageSnapshot('split-channels')
        done()
      })
    })
  })

  it('should split channels with options', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/stereo.mp3',
        splitChannels: [
          {
            waveColor: 'rgb(200, 0, 200)',
            progressColor: 'rgb(100, 0, 100)',
          },
          {
            waveColor: 'rgb(0, 200, 200)',
            progressColor: 'rgb(0, 100, 100)',
          },
        ],
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('split-channels-options')
        done()
      })
    })
  })

  it('should split channels with individual channel heights', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/stereo.mp3',
        splitChannels: [
          {
            waveColor: 'red',
            height: 60,
          },
          {
            waveColor: 'blue',
            height: 30,
          },
        ],
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('split-channels-heights')
        done()
      })
    })
  })

  it('should use plugins with Regions', (done) => {
    cy.window().then((win) => {
      const regions = win.Regions.create()

      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        plugins: [regions],
      })

      wrapReady(wavesurfer).then(() => {
        regions.addRegion({
          start: 1,
          end: 3,
          color: 'rgba(255, 0, 0, 0.1)',
        })

        cy.get(id).matchImageSnapshot('plugins-regions')
        done()
      })
    })
  })

  it('should use two plugins: Regions and Timeline', (done) => {
    cy.window().then((win) => {
      const regions = win.Regions.create()

      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        plugins: [regions, win.Timeline.create()],
      })

      wrapReady(wavesurfer).then(() => {
        regions.addRegion({
          start: 1,
          end: 3,
          color: 'rgba(255, 0, 0, 0.1)',
        })

        cy.get(id).matchImageSnapshot('plugins-regions-timeline')
        done()
      })
    })
  })

  it('should normalize', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        normalize: true,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('normalize')
        done()
      })
    })
  })

  it('should not create an extra canvas when using bars with normalize', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barWidth: 2,
        normalize: true,
      })

      wrapReady(wavesurfer).then(() => {
        const canvases = wavesurfer.getWrapper().querySelectorAll('canvas')
        expect(canvases.length).to.equal(2)
        done()
      })
    })
  })

  it('should use height', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        height: 10,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('height-10')
        done()
      })
    })
  })

  it('should use parent height if height is auto', (done) => {
    cy.window().then((win) => {
      win.document.querySelector(id).style.height = '200px'

      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        height: 'auto',
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('height-auto')
        win.document.querySelector(id).style.height = ''
        done()
      })
    })
  })

  it('should fall back to 128 if container height is not set', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        height: 'auto',
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('height-auto-0')
        done()
      })
    })
  })

  it('should use a custom rendering function', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        renderFunction: (channels, ctx) => {
          const { width, height } = ctx.canvas
          const scale = channels[0].length / width
          const step = 10

          ctx.translate(0, height / 2)
          ctx.strokeStyle = ctx.fillStyle
          ctx.beginPath()

          for (let i = 0; i < width; i += step * 2) {
            const index = Math.floor(i * scale)
            const value = Math.abs(channels[0][index])
            let x = i
            let y = value * height

            ctx.moveTo(x, 0)
            ctx.lineTo(x, y)
            ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, true)
            ctx.lineTo(x + step, 0)

            x = x + step
            y = -y
            ctx.moveTo(x, 0)
            ctx.lineTo(x, y)
            ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, false)
            ctx.lineTo(x + step, 0)
          }

          ctx.stroke()
          ctx.closePath()
        },
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('custom-render')
        done()
      })
    })
  })

  it('should pass custom parameters to fetch', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        fetchParams: {
          headers: {
            'X-Custom-Header': 'foo',
          },
        },
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('fetch-options')
        done()
      })
    })
  })

  it('should remount the container when set via setOptions', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        barWidth: 4,
        barGap: 3,
        barRadius: 4,
      })

      wrapReady(wavesurfer).then(() => {
        wavesurfer.setOptions({ container: otherId })
        cy.get(id).children().should('have.length', 0)
        cy.get(otherId).children().should('have.length', 1)
        cy.get(otherId).matchImageSnapshot('bars')
        done()
      })
    })
  })

  it('should accept a numeric width option', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        width: 100,
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('width-100')
        wavesurfer.setOptions({ width: 300 })
        cy.get(id).matchImageSnapshot('width-300')
        done()
      })
    })
  })

  it('should accept a CSS value for the width option', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        width: '10rem',
      })

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('width-10rem')
        wavesurfer.setOptions({ width: '200px' })
        cy.get(id).matchImageSnapshot('width-200px')
        done()
      })
    })
  })

  it('should render pre-decoded waveform w/o audio', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        peaks: new Array(512).fill(0.5).map((v, i) => v * Math.sin(i / 16)),
        duration: 12.5,
      })

      wrapReady(wavesurfer, 'redraw').then(() => {
        expect(wavesurfer.getDuration().toFixed(2)).to.equal('12.50')
        cy.get(id).matchImageSnapshot('pre-decoded-no-audio')
        done()
      })
    })
  })

  it('should support Web Audio playback', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        url: '../../examples/audio/demo.wav',
        backend: 'WebAudio',
      })

      wrapReady(wavesurfer).then(() => {
        expect(wavesurfer.getDuration().toFixed(2)).to.equal('21.77')
        wavesurfer.setTime(10)
        expect(wavesurfer.getCurrentTime().toFixed(2)).to.equal('10.00')
        wavesurfer.setTime(21.6)
        wavesurfer.play()
      })

      wavesurfer.once('finish', () => {
        done()
      })
    })
  })

  it('should load a blob', (done) => {
    cy.window().then((win) => {
      const wavesurfer = win.WaveSurfer.create({
        container: id,
        height: 100,
      })

      const blob = Cypress.Blob.base64StringToBlob(
        'UklGRuYAAABXQVZFZm10IBAAAAABAAEAgD4AAAB9AAACABAAZGF0YQAAAAA=',
        'audio/wav',
      )

      wavesurfer.loadBlob(
        blob,
        Array.from({ length: 512 }).map((_, i) => Math.sin(i / 16)),
        10,
      )

      wrapReady(wavesurfer).then(() => {
        cy.get(id).matchImageSnapshot('loadBlob')
        done()
      })
    })
  })

  it('should render in a very wide container', (done) => {
    cy.window().then((win) => {
      const container = win.document.querySelector(id)
      container.style.width = '21000px'

      const wavesurfer = win.WaveSurfer.create({
        container,
        url: '../../examples/audio/demo.wav',
        peaks: new Array(512).fill(0.5).map((v, i) => v * Math.sin(i / 16)),
      })

      cy.get(id).matchImageSnapshot('very-wide-container')

      wrapReady(wavesurfer).then(() => {
        container.style.width = ''
        done()
      })
    })
  })
})


================================================
FILE: cypress/e2e/regions-no-audio.cy.js
================================================
describe('WaveSurfer Regions plugin with no audio tests', () => {
  beforeEach((done) => {
    cy.visit('cypress/e2e/index.html')

    cy.window().its('WaveSurfer').should('exist')

    cy.window().then((win) => {
      const waitForReady = new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',

          // For these tests we're explicitly testing for scenarios where audio is not loaded
          // so we don't pass a url, instead we use peaks & duration.
          //url: '/examples/audio/demo.wav',

          duration: 22,
          peaks: [
            [
              0, 0.0023595101665705442, 0.012107174843549728, 0.005919494666159153, -0.31324470043182373,
              0.1511787623167038, 0.2473851442337036, 0.11443428695201874, -0.036057762801647186, -0.0968964695930481,
              -0.03033737652003765, 0.10682467371225357, 0.23974689841270447, 0.013210971839725971,
              -0.12377244979143143, 0.046145666390657425, -0.015757400542497635, 0.10884027928113937,
              0.06681904196739197, 0.09432944655418396, -0.17105795443058014, -0.023439358919858932,
              -0.10380347073078156, 0.0034454423002898693, 0.08061369508504868, 0.026129156351089478,
              0.18730352818965912, 0.020447958260774612, -0.15030759572982788, 0.05689578503370285,
              -0.0009095853311009705, 0.2749626338481903, 0.2565386891365051, 0.07571295648813248, 0.10791446268558502,
              -0.06575305759906769, 0.15336275100708008, 0.07056761533021927, 0.03287476301193237, -0.09044631570577621,
              0.01777501218020916, -0.04906218498945236, -0.04756792634725571, -0.006875281687825918,
              0.04520256072282791, -0.02362387254834175, -0.0668797641992569, 0.12266506254673004, -0.10895221680402756,
              0.03791835159063339, -0.0195105392485857, -0.031097881495952606, 0.04252675920724869,
              -0.09187793731689453, 0.0829525887966156, -0.003812957089394331, 0.0431736595928669, 0.07634212076663971,
              -0.05335947126150131, 0.0345163568854332, -0.049201950430870056, 0.02300390601158142,
              0.007677287794649601, 0.015354577451944351, 0.007677287794649601, 0.007677288725972176,
            ],
          ],
          splitChannels: true,
          plugins: [win.Regions.create()],
        })

        win.wavesurfer.once('ready', () => resolve())
      })

      cy.wrap(waitForReady).then(done)
    })
  })

  it('should listen to events on a region', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      const region = regionsPlugin.addRegion({
        start: 1,
        end: 3,
        content: 'Test region',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.element.textContent).to.equal('Test region')

      let eventHandlerCalled = false

      regionsPlugin.on('region-in', (reg, e) => {
        expect(region).to.equal(reg)
        eventHandlerCalled = true
      })

      win.wavesurfer.setTime(2)

      expect(eventHandlerCalled).to.be.true
      expect(win.wavesurfer.isPlaying()).to.be.false
      expect(win.wavesurfer.getCurrentTime()).to.equal(2)

      win.wavesurfer.destroy()
    })
  })
})


================================================
FILE: cypress/e2e/regions.cy.js
================================================
describe('WaveSurfer Regions plugin tests', () => {
  beforeEach((done) => {
    cy.visit('cypress/e2e/index.html')

    cy.window().its('WaveSurfer').should('exist')

    cy.window().then((win) => {
      const waitForReady = new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          height: 200,
          waveColor: 'rgb(200, 200, 0)',
          progressColor: 'rgb(100, 100, 0)',
          url: '../../examples/audio/stereo.mp3',
          splitChannels: true,
          plugins: [win.Regions.create()],
        })

        win.wavesurfer.once('ready', () => resolve())
      })

      cy.wrap(waitForReady).then(done)
    })
  })

  it('should create and remove regions', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]

      expect(regions).to.be.an('object')

      // Add a region
      const color = 'rgba(100, 0, 0, 0.1)'
      const firstRegion = regions.addRegion({
        start: 1.5,
        end: 10.1,
        content: 'Hello',
        color,
      })

      expect(firstRegion).to.be.an('object')
      expect(firstRegion.element).to.be.an('HTMLDivElement')
      expect(firstRegion.element.textContent).to.equal('Hello')
      expect(firstRegion.element.style.backgroundColor).to.equal(color)

      firstRegion.remove()
      expect(firstRegion.element).to.be.null

      // Create another region
      const secondColor = 'rgba(0, 0, 100, 0.1)'
      const secondRegion = regions.addRegion({
        start: 5.8,
        end: 12,
        content: 'Second',
        color: secondColor,
      })

      expect(secondRegion).to.be.an('object')
      expect(secondRegion.element).to.be.an('HTMLDivElement')
      expect(secondRegion.element.textContent).to.equal('Second')
      expect(secondRegion.element.style.backgroundColor).to.equal(secondColor)

      secondRegion.remove()
      expect(secondRegion.element).to.be.null
    })
  })

  it('should drag a region', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]
      const region = regions.addRegion({
        start: 3,
        end: 8,
        content: 'Region',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.start).to.equal(3)

      return cy.wait(10).then(() => {
        // Drag the region
        const pointerDownEvent = new PointerEvent('pointerdown', {
          clientX: 90,
          clientY: 1,
        })
        const pointerMoveEvent = new PointerEvent('pointermove', {
          clientX: 200,
          clientY: 10,
        })
        const pointerUpEvent = new PointerEvent('pointerup', {
          clientX: 200,
          clientY: 10,
        })
        region.element.dispatchEvent(pointerDownEvent)
        win.document.dispatchEvent(pointerMoveEvent)
        win.document.dispatchEvent(pointerUpEvent)

        expect(region.start).to.be.greaterThan(3)
      })
    })
  })

  it('should set the color of a region', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]
      const region = regions.addRegion({
        start: 3,
        end: 8,
        content: 'Region',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.color).to.equal('rgba(0, 100, 0, 0.2)')

      region.setOptions({ color: 'rgba(100, 0, 0, 0.1)' })

      expect(region.color).to.equal('rgba(100, 0, 0, 0.1)')

      region.remove()
    })
  })

  it('should set a region position', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]
      const region = regions.addRegion({
        start: 3,
        end: 8,
        content: 'Region',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.start).to.equal(3)
      expect(region.end).to.equal(8)
      expect(region.resize).to.equal(true)

      region.setOptions({
        start: 5,
        end: 10,
        resize: false,
      })

      expect(region.start).to.equal(5)
      expect(region.end).to.equal(10)
      expect(region.resize).to.equal(false)
    })
  })

  it('should create markers', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]
      const region = regions.addRegion({ start: 3, content: 'Marker', color: 'rgba(0, 100, 100, 0.2)' })
      expect(region.start).to.equal(3)
      expect(region.end).to.equal(3)
      expect(region.element.style.backgroundColor).to.equal('')
    })
  })

  it('should allow drag selection', () => {
    cy.window().then((win) => {
      const regions = win.wavesurfer.getActivePlugins()[0]

      const disableDragSelection = regions.enableDragSelection({
        color: 'rgba(0, 100, 0, 0.2)',
        content: 'Drag',
      })

      expect(regions.getRegions().length).to.equal(0)

      regions.addRegion({
        start: 3,
        end: 8,
        content: 'Region',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      let regionInitializedEventCalled = false
      regions.on('region-initialized', () => {
        regionInitializedEventCalled = true
      })

      expect(regions.getRegions().length).to.equal(1)

      // Drag the region
      const pointerDownEvent = new PointerEvent('pointerdown', {
        clientX: 40,
        clientY: 1,
      })
      const pointerMoveEvent = new PointerEvent('pointermove', {
        clientX: 100,
        clientY: 10,
      })
      const pointerUpEvent = new PointerEvent('pointerup', {
        clientX: 100,
        clientY: 10,
      })
      win.wavesurfer.getWrapper().dispatchEvent(pointerDownEvent)
      win.document.dispatchEvent(pointerMoveEvent)
      expect(regionInitializedEventCalled).to.be.true
      win.document.dispatchEvent(pointerUpEvent)

      // It shouldn't trigger a click
      expect(win.wavesurfer.getCurrentTime()).to.equal(0)

      expect(regions.getRegions().length).to.equal(2)
      expect(regions.getRegions()[1].element.textContent).to.equal('Drag')
      regions.clearRegions()
      expect(regions.getRegions().length).to.equal(0)

      // Disable drag selection
      disableDragSelection()

      win.wavesurfer.getWrapper().querySelector('div').dispatchEvent(pointerDownEvent)
      win.document.dispatchEvent(pointerMoveEvent)
      win.document.dispatchEvent(pointerUpEvent)

      // It should not create any regions because drag selection is disabled
      expect(regions.getRegions().length).to.equal(0)
    })
  })

  it('should listen to clicks on a region', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      const region = regionsPlugin.addRegion({
        start: 1,
        end: 5,
        content: 'Click me',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.element.textContent).to.equal('Click me')

      region.on('click', (e) => {
        e.stopPropagation()
      })

      regionsPlugin.on('region-clicked', (reg, e) => {
        expect(e.stopPropagation instanceof Function).to.be.true
        expect(region).to.equal(reg)
        reg.play()
      })

      // Should not trigger an interaction on the wavesurfer
      win.wavesurfer.on('interaction', () => {
        expect(false).to.be.true
      })

      const clickEvent = new Event('click')

      region.element.dispatchEvent(clickEvent)

      expect(win.wavesurfer.isPlaying()).to.be.true
      expect(win.wavesurfer.getCurrentTime()).to.equal(region.start)

      win.wavesurfer.destroy()
    })
  })

  it('should set region content', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      const region = regionsPlugin.addRegion({
        start: 1,
        end: 5,
        content: 'Click me',
        color: 'rgba(0, 100, 0, 0.2)',
      })

      expect(region.element.textContent).to.equal('Click me')

      region.setOptions({ content: 'Updated' })

      expect(region.element.textContent).to.equal('Updated')

      region.setContent('Updated again')

      expect(region.element.textContent).to.equal('Updated again')

      // HTML content
      const div = document.createElement('div')
      div.innerHTML = '<p>HTML content</p>'
      region.setContent(div)

      expect(region.element.textContent).to.equal('HTML content')

      win.wavesurfer.destroy()
    })
  })

  it('should set region id', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      const region = regionsPlugin.addRegion({
        id: 'my-region',
        start: 1,
        end: 5,
      })

      expect(region.element.getAttribute('part')).to.equal('region my-region')

      // Make it a marker
      region.setOptions({ start: 3, end: 3 })

      expect(region.element.getAttribute('part')).to.equal('marker my-region')

      // Set the id
      region.setOptions({ id: 'my-marker' })

      expect(region.id).to.equal('my-marker')

      expect(region.element.getAttribute('part')).to.equal('marker my-marker')

      win.wavesurfer.destroy()
    })
  })

  it('should not add resize handles if resize is set to false', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      const region = regionsPlugin.addRegion({
        id: 'no-resize-region',
        start: 1,
        end: 5,
        resize: false,
      })

      expect(region).to.be.an('object')
      expect(region.element).to.be.an('HTMLDivElement')
      expect(region.element.children).to.have.length(0)

      win.wavesurfer.destroy()
    })
  })

  it('should add a region to a specific channel by index', () => {
    cy.window().then((win) => {
      const regionsPlugin = win.wavesurfer.getActivePlugins()[0]

      regionsPlugin.addRegion({
        start: 25,
        end: 100,
        channelIdx: 1,
      })

      cy.get('#waveform').matchImageSnapshot('regions-channelIdx')
    })
  })
})


================================================
FILE: cypress/e2e/spectrogram.cy.js
================================================
const id = '#waveform'
const scales = ['linear', 'mel', 'log', 'bark', 'erb']

xdescribe('WaveSurfer Spectrogram plugin tests', () => {
  it('should render a spectrogram', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 200,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Spectrogram.create({
              height: 200,
              labels: true,
              scale: 'linear',
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          cy.get(id).matchImageSnapshot('spectrogram-basic')
          resolve()
        })
      })
    })
  })

  it('should render a spectrogram without labels', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 200,
          url: '../../examples/audio/demo.wav',
          plugins: [
            win.Spectrogram.create({
              height: 200,
              labels: false,
              scale: 'linear',
            }),
          ],
        })

        win.wavesurfer.once('ready', () => {
          cy.get(id).matchImageSnapshot('spectrogram-no-labels')
          resolve()
        })
      })
    })
  })

  it('should render a spectrogram when initialised into a hidden div', () => {
    cy.visit('cypress/e2e/index.html')
    cy.window().then((win) => {
      return new Promise((resolve) => {
        // Hide the wavesurfer div and initialise
        win.document.querySelector(id).style.display = 'none'
        win.wavesurfer = win.WaveSurfer.create({
          container: id,
          height: 200,
          plugins: [
            win.Spectrogram.create({
              height: 200,
              labels: true,
              scale: 'linear',
            }),
          ],
        })

        // Load a file and unhide the div
        win.wavesurfer.load('../../examples/audio/demo.wav')
        win.document.querySelector(id).style.display = 'inline-block'

        // Ensure we display the spectrogram successfully
        win.wavesurfer.once('ready', () => {
          cy.get(id).matchImageSnapshot('spectrogram-unhidden')
          resolve()
        })
      })
    })
  })

  scales.forEach((scale) => {
    it(`should display correct frequency labels with 1kHz tone (${scale})`, () => {
      cy.visit('cypress/e2e/index.html')
      cy.window().then((win) => {
        return new Promise((resolve) => {
          win.wavesurfer = win.WaveSurfer.create({
            container: id,
            height: 200,
            url: '../../examples/audio/1khz.mp3',
            plugins: [
              win.Spectrogram.create({
                height: 200,
                labels: true,
                scale: scale,
                frequencyMin: 0,
                frequencyMax: 4000,
                splitChannels: false,
              }),
            ],
          })

          win.wavesurfer.once('ready', () => {
            cy.get(id).matchImageSnapshot(`spectrogram-1khz-${scale}`)
            resolve()
          })
        })
      })
    })
  })
})


================================================
FILE: cypress/e2e/umd.cy.js
================================================
describe('WaveSurfer UMD module tests', () => {
  beforeEach(() => {
    cy.visit('cypress/e2e/umd.html')
    cy.window().its('WaveSurfer').should('exist')
  })

  it('should instantiate WaveSurfer with two plugins', () => {
    cy.window().then((win) => {
      return new Promise((resolve) => {
        const { WaveSurfer } = win
        win.wavesurfer = win.WaveSurfer.create({
          container: '#waveform',
          url: '../../examples/audio/demo.wav',
          plugins: [WaveSurfer.Regions.create(), WaveSurfer.Timeline.create()],
        })
        resolve()
      })
    })
  })
})


================================================
FILE: cypress/e2e/umd.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WaveSurfer CommonJS Test</title>

    <script type="text/javascript" src="../../dist/wavesurfer.min.js"></script>
    <script type="text/javascript" src="../../dist/plugins/regions.min.js"></script>
    <script type="text/javascript" src="../../dist/plugins/timeline.min.js"></script>
  </head>
  <body>
    <div id="waveform" style="width: 600px"></div>
  </body>
</html>


================================================
FILE: cypress/e2e/webaudio.cy.js
================================================
import WebAudioPlayer from '../../dist/webaudio.js'

describe('WebAudioPlayer', () => {
  beforeEach(() => {
    cy.window().then((win) => {
      // Create fresh mock objects for each test
      const mockBufferNode = {
        buffer: null,
        connect: cy.stub(),
        disconnect: cy.stub(),
        start: cy.stub(),
        stop: cy.stub(),
        playbackRate: { value: 1 },
        onended: null,
      }

      const mockGainNode = {
        connect: cy.stub(),
        gain: { value: 1 },
      }

      const mockAudioContext = {
        currentTime: 0,
        createBufferSource: cy.stub().returns(mockBufferNode),
        createGain: cy.stub().returns(mockGainNode),
        destination: {},
      }

      // Create a fresh WebAudioPlayer instance
      const player = new WebAudioPlayer(mockAudioContext)
      win.WebAudioPlayer = player // Attach to window for Cypress access

      // Set up a mock buffer for playback
      const mockBuffer = { duration: 10 }
      player.buffer = mockBuffer

      // Store objects as Cypress aliases for easy access
      cy.wrap(player).as('player')
      cy.wrap(mockBufferNode.start).as('startStub')
    })
  })

  describe('_play method', () => {
    it('should reset position when currentPos is negative', () => {
      cy.get('@player').then((player) => {
        player.playbackRate = 1
        player.currentTime = -1

        return player.play().then(() => {
          // Verify position was reset
          expect(player.currentTime).to.equal(0)
          cy.get('@startStub').should('have.been.calledWith', 0, 0)
        })
      })
    })

    it('should reset position when currentPos exceeds duration', () => {
      cy.get('@player').then((player) => {
        player.currentTime = 15

        return player.play().then(() => {
          // Verify position was reset
          expect(player.currentTime).to.equal(0)
          cy.get('@startStub').should('have.been.calledWith', 0, 0)
        })
      })
    })

    it('should maintain position when within valid range', () => {
      cy.get('@player').then((player) => {
        const validTime = 5
        player.currentTime = validTime

        return player.play().then(() => {
          // Verify position was maintained
          cy.get('@startStub').should('have.been.calledWith', 0, validTime)
        })
      })
    })

    it('should handle playback rate changes correctly', () => {
      cy.get('@player').then((player) => {
        player.currentTime = 2
        player.playbackRate = 2

        return player.play().then(() => {
          // currentPos should be 2 (playbackRate affects speed, not start offset)
          cy.get('@startStub').should('have.been.calledWith', 0, 2)
          expect(player.bufferNode.playbackRate.value).to.equal(2)
        })
      })
    })
  })
})


================================================
FILE: cypress/support/commands.ts
================================================
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
//   namespace Cypress {
//     interface Chainable {
//       login(email: string, password: string): Chainable<void>
//       drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
//       dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
//       visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
//     }
//   }
// }

import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command'

addMatchImageSnapshotCommand({
  failureThresholdType: 'percent',
  failureThreshold: 0.03,
})


================================================
FILE: cypress/support/e2e.ts
================================================
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')


================================================
FILE: cypress.config.js
================================================
import { defineConfig } from 'cypress'
import { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin.js'

export default defineConfig({
  video: false,
  e2e: {
    setupNodeEvents(on, config) {
      on('before:browser:launch', (browser, launchOptions) => {
        if (browser.name === 'chrome' || browser.name === 'chromium') {
          launchOptions.args.push('--force-device-scale-factor=1')
        }
        return launchOptions
      })

      addMatchImageSnapshotPlugin(on, config)
    },
  },
})


================================================
FILE: eslint.config.js
================================================
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'

const compat = new FlatCompat({
  baseDirectory: import.meta.dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
})

export default compat.config({
  env: { browser: true, es2021: true },
  parser: '@typescript-eslint/parser',
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  plugins: ['@typescript-eslint', 'prettier'],
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'],
  rules: {
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-empty-object-type': 'off',
    '@typescript-eslint/no-this-alias': 'off',
  },
  ignorePatterns: ['cypress', 'examples', 'tutorial', 'src/plugins/spectrogram*', 'scripts'],
  overrides: [
    {
      files: ['src/__tests__/**/*.ts'],
      env: { jest: true, node: true },
      rules: {
        '@typescript-eslint/ban-ts-comment': 'off',
      },
    },
  ],
})


================================================
FILE: examples/_preview.js
================================================
const iframe = document.querySelector('iframe')
const textarea = document.querySelector('textarea')

const loadPreview = (code) => {
  const html = code.replace(/\n/g, '').match(/<html>(.+?)<\/html>/gm) || []
  const script = code
    .replace(/<\/script>/g, '')
    .replace(/'wavesurfer.js'/g, `'../dist/wavesurfer.esm.js'`)
    .replace(/'wavesurfer.js/g, `'..`)
    .replace(/\.esm\.js/g, '.js')
  const isBabel = script.includes('@babel')

  // Start of iframe template
  iframe.srcdoc = `
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>wavesurfer.js examples</title>
    <style>
      html {
        font-family: sans-serif;
      }
      body {
        margin: 0;
        padding: 1rem;
      }
      @media (prefers-color-scheme: dark) {
        body {
          background: #333;
          color: #eee;
        }
        a {
          color: #fff;
        }
      }
      input {
        vertical-align: middle;
      }
    </style>
  </head>

  <body>
    ${html.join('')}

    <script type="${isBabel ? 'text/babel' : 'module'}" data-type="module">
      ${script}
    </script>
  </body>
</html>
`
  // End of iframe template
}

const openExample = (url) => {
  fetch(`/examples/${url}`, {
    cache: 'no-cache',
  })
    .then((res) => res.text())
    .then((text) => {
      loadPreview(text)
      textarea.value = text
    })
}

let delay
document.querySelector('textarea').addEventListener('input', (e) => {
  if (delay) clearTimeout(delay)
  delay = setTimeout(() => {
    loadPreview(e.target.value)
  }, 500)
})

const url = location.hash.slice(1) || 'basic.js'
openExample(url)

let active = document.querySelector(`aside a[href="#${url}"]`)
if (active) active.classList.add('active')
document.querySelectorAll('aside a').forEach((link) => {
  link.addEventListener('click', () => {
    const url = link.hash.slice(1)
    openExample(url)
    if (active) active.classList.remove('active')
    active = link
    active.classList.add('active')
  })
})


================================================
FILE: examples/all-options.js
================================================
// All wavesurfer options in one place

import WaveSurfer from 'wavesurfer.js'

const options = {
  /** HTML element or CSS selector (required) */
  container: 'body',
  /** The height of the waveform in pixels */
  height: 128,
  /** The width of the waveform in pixels or any CSS value; defaults to 100% */
  width: 300,
  /** Render each audio channel as a separate waveform */
  splitChannels: false,
  /** Stretch the waveform to the full height */
  normalize: false,
  /** The color of the waveform */
  waveColor: '#ff4e00',
  /** The color of the progress mask */
  progressColor: '#dd5e98',
  /** The color of the playback cursor */
  cursorColor: '#ddd5e9',
  /** The cursor width */
  cursorWidth: 2,
  /** Render the waveform with bars like this: ▁ ▂ ▇ ▃ ▅ ▂ */
  barWidth: NaN,
  /** Spacing between bars in pixels */
  barGap: NaN,
  /** Rounded borders for bars */
  barRadius: NaN,
  /** A vertical scaling factor for the waveform */
  barHeight: NaN,
  /** Vertical bar alignment **/
  barAlign: '',
  /** Minimum pixels per second of audio (i.e. zoom level) */
  minPxPerSec: 1,
  /** Stretch the waveform to fill the container, true by default */
  fillParent: true,
  /** Audio URL */
  url: '/examples/audio/audio.wav',
  /** Whether to show default audio element controls */
  mediaControls: true,
  /** Play the audio on load */
  autoplay: false,
  /** Pass false to disable clicks on the waveform */
  interact: true,
  /** Allow to drag the cursor to seek to a new position */
  dragToSeek: false,
  /** Hide the scrollbar */
  hideScrollbar: false,
  /** Audio rate */
  audioRate: 1,
  /** Automatically scroll the container to keep the current position in viewport */
  autoScroll: true,
  /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
  autoCenter: true,
  /** Decoding sample rate. Doesn't affect the playback. Defaults to 8000 */
  sampleRate: 8000,
}

const wavesurfer = WaveSurfer.create(options)

wavesurfer.on('ready', () => {
  wavesurfer.setTime(10)
})

// Generate a form input for each option
const schema = {
  height: {
    value: 128,
    min: 10,
    max: 512,
    step: 1,
  },
  width: {
    value: 300,
    min: 10,
    max: 2000,
    step: 1,
  },
  cursorWidth: {
    value: 1,
    min: 0,
    max: 10,
    step: 1,
  },
  minPxPerSec: {
    value: 1,
    min: 1,
    max: 1000,
    step: 1,
  },
  barWidth: {
    value: 0,
    min: 1,
    max: 30,
    step: 1,
  },
  barHeight: {
    value: 1,
    min: 0.1,
    max: 4,
    step: 0.1,
  },
  barGap: {
    value: 0,
    min: 1,
    max: 30,
    step: 1,
  },
  barRadius: {
    value: 0,
    min: 1,
    max: 30,
    step: 1,
  },
  peaks: {
    type: 'json',
  },
  audioRate: {
    value: 1,
    min: 0.1,
    max: 4,
    step: 0.1,
  },
  sampleRate: {
    value: 8000,
    min: 8000,
    max: 48000,
    step: 1000,
  },
}

const form = document.createElement('form')
Object.assign(form.style, {
  display: 'flex',
  flexDirection: 'column',
  gap: '1rem',
  padding: '1rem',
})
document.body.appendChild(form)

for (const key in options) {
  if (options[key] === undefined) continue
  const isColor = key.includes('Color')

  const label = document.createElement('label')
  Object.assign(label.style, {
    display: 'flex',
    alignItems: 'center',
  })

  const span = document.createElement('span')
  Object.assign(span.style, {
    textTransform: 'capitalize',
    width: '7em',
  })
  span.textContent = `${key.replace(/[a-z0-9](?=[A-Z])/g, '$& ')}: `
  label.appendChild(span)

  const input = document.createElement('input')
  const type = typeof options[key]
  Object.assign(input, {
    type: isColor ? 'color' : type === 'number' ? 'range' : type === 'boolean' ? 'checkbox' : 'text',
    name: key,
    value: options[key],
    checked: options[key] === true,
  })
  if (input.type === 'text') input.style.flex = 1
  if (options[key] instanceof HTMLElement) input.disabled = true

  if (schema[key]) {
    Object.assign(input, schema[key])
  }

  label.appendChild(input)
  form.appendChild(label)

  input.oninput = () => {
    if (type === 'number') {
      options[key] = input.valueAsNumber
    } else if (type === 'boolean') {
      options[key] = input.checked
    } else if (schema[key] && schema[key].type === 'json') {
      options[key] = JSON.parse(input.value)
    } else {
      options[key] = input.value
    }
    wavesurfer.setOptions(options)
    textarea.value = JSON.stringify(options, null, 2)
  }
}

const textarea = document.createElement('textarea')
Object.assign(textarea.style, {
  width: '100%',
  height: Object.keys(options).length + 1 + 'rem',
})
textarea.value = JSON.stringify(options, null, 2)
textarea.readOnly = true
form.appendChild(textarea)


================================================
FILE: examples/audio/reed.sh
================================================
#!/bin/bash

say -v 'Reed (English (US))' -o "${1}" --file-format='mp4f' "${1}"


================================================
FILE: examples/bars.js
================================================
// SoundCloud-style bars

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',

  // Set a bar width
  barWidth: 2,
  // Optionally, specify the spacing between bars
  barGap: 1,
  // And the bar radius
  barRadius: 2,
})

wavesurfer.once('interaction', () => {
  wavesurfer.play()
})


================================================
FILE: examples/basic.js
================================================
// A basic example

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/demo.wav',
})

wavesurfer.on('click', () => {
  wavesurfer.play()
})


================================================
FILE: examples/custom-render.js
================================================
// Custom rendering function

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/demo.wav',

  /**
   * Render a waveform as a squiggly line
   * @see https://css-tricks.com/making-an-audio-waveform-visualizer-with-vanilla-javascript/
   */
  renderFunction: (channels, ctx) => {
    const { width, height } = ctx.canvas
    const scale = channels[0].length / width
    const step = 10

    ctx.translate(0, height / 2)
    ctx.strokeStyle = ctx.fillStyle
    ctx.beginPath()

    for (let i = 0; i < width; i += step * 2) {
      const index = Math.floor(i * scale)
      const value = Math.abs(channels[0][index])
      let x = i
      let y = value * height

      ctx.moveTo(x, 0)
      ctx.lineTo(x, y)
      ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, true)
      ctx.lineTo(x + step, 0)

      x = x + step
      y = -y
      ctx.moveTo(x, 0)
      ctx.lineTo(x, y)
      ctx.arc(x + step / 2, y, step / 2, Math.PI, 0, false)
      ctx.lineTo(x + step, 0)
    }

    ctx.stroke()
    ctx.closePath()
  },
})

wavesurfer.on('interaction', () => {
  wavesurfer.play()
})


================================================
FILE: examples/envelope.js
================================================
// Envelope plugin
// Graphical fade-in and fade-out and volume control

/*
<html>
  <button style="min-width: 5em" id="play">Play</button>
  <button style="margin: 0 1em 2em" id="randomize">Randomize points</button>

  Volume: <label>0</label>
  <div id="container" style="border: 1px solid #ddd;"></div>
  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_envelope.EnvelopePlugin">Envelope plugin docs</a>
  </p>
</html>
*/

import WaveSurfer from 'wavesurfer.js'
import EnvelopePlugin from 'wavesurfer.js/dist/plugins/envelope.esm.js'

// Create an instance of WaveSurfer
const wavesurfer = WaveSurfer.create({
  container: '#container',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
})

const isMobile = top.matchMedia('(max-width: 900px)').matches

// Initialize the Envelope plugin
const envelope = wavesurfer.registerPlugin(
  EnvelopePlugin.create({
    volume: 0.8,
    lineColor: 'rgba(255, 0, 0, 0.5)',
    lineWidth: 4,
    dragPointSize: isMobile ? 20 : 12,
    dragLine: !isMobile,
    dragPointFill: 'rgba(0, 255, 255, 0.8)',
    dragPointStroke: 'rgba(0, 0, 0, 0.5)',

    points: [
      { time: 11.2, volume: 0.5 },
      { time: 15.5, volume: 0.8 },
    ],
  }),
)

envelope.on('points-change', (points) => {
  console.log('Envelope points changed', points)
})

envelope.addPoint({ time: 1, volume: 0.9 })

// Randomize points
const randomizePoints = () => {
  const points = []
  const len = 5 * Math.random()
  for (let i = 0; i < len; i++) {
    points.push({
      time: Math.random() * wavesurfer.getDuration(),
      volume: Math.random(),
    })
  }
  envelope.setPoints(points)
}

document.querySelector('#randomize').onclick = randomizePoints

// Show the current volume
const volumeLabel = document.querySelector('label')
const showVolume = () => {
  volumeLabel.textContent = envelope.getCurrentVolume().toFixed(2)
}
envelope.on('volume-change', showVolume)
wavesurfer.on('ready', showVolume)

// Play/pause button
const button = document.querySelector('#play')
wavesurfer.once('ready', () => {
  button.onclick = () => {
    wavesurfer.playPause()
  }
})
wavesurfer.on('play', () => {
  button.textContent = 'Pause'
})
wavesurfer.on('pause', () => {
  button.textContent = 'Play'
})


================================================
FILE: examples/events.js
================================================
import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
})

/** When audio starts loading */
wavesurfer.on('load', (url) => {
  console.log('Load', url)
})

/** During audio loading */
wavesurfer.on('loading', (percent) => {
  console.log('Loading', percent + '%')
})

/** When the audio has been decoded */
wavesurfer.on('decode', (duration) => {
  console.log('Decode', duration + 's')
})

/** When the audio is both decoded and can play */
wavesurfer.on('ready', (duration) => {
  console.log('Ready', duration + 's')
})

/** When visible waveform is drawn */
wavesurfer.on('redraw', () => {
  console.log('Redraw began')
})

/** When all audio channel chunks of the waveform have drawn */
wavesurfer.on('redrawcomplete', () => {
  console.log('Redraw complete')
})

/** When the audio starts playing */
wavesurfer.on('play', () => {
  console.log('Play')
})

/** When the audio pauses */
wavesurfer.on('pause', () => {
  console.log('Pause')
})

/** When the audio finishes playing */
wavesurfer.on('finish', () => {
  console.log('Finish')
})

/** On audio position change, fires continuously during playback */
wavesurfer.on('timeupdate', (currentTime) => {
  console.log('Time', currentTime + 's')
})

/** When the user seeks to a new position */
wavesurfer.on('seeking', (currentTime) => {
  console.log('Seeking', currentTime + 's')
})

/** When the user interacts with the waveform (i.g. clicks or drags on it) */
wavesurfer.on('interaction', (newTime) => {
  console.log('Interaction', newTime + 's')
})

/** When the user clicks on the waveform */
wavesurfer.on('click', (relativeX) => {
  console.log('Click', relativeX)
})

/** When the user drags the cursor */
wavesurfer.on('drag', (relativeX) => {
  console.log('Drag', relativeX)
})

/** When the waveform is scrolled (panned) */
wavesurfer.on('scroll', (visibleStartTime, visibleEndTime) => {
  console.log('Scroll', visibleStartTime + 's', visibleEndTime + 's')
})

/** When the zoom level changes */
wavesurfer.on('zoom', (minPxPerSec) => {
  console.log('Zoom', minPxPerSec + 'px/s')
})

/** Just before the waveform is destroyed so you can clean up your events */
wavesurfer.on('destroy', () => {
  console.log('Destroy')
})

wavesurfer.load('/examples/audio/audio.wav')

/*
<html>
  <label>
    Zoom: <input type="range" min="10" max="1000" value="100" />
  </label>
  <button>Play/pause</button>
  <p>Open the console to see the event logs</p>
</html>
*/

// Update the zoom level on slider change
wavesurfer.once('decode', () => {
  const slider = document.querySelector('input[type="range"]')

  slider.addEventListener('input', (e) => {
    const minPxPerSec = e.target.valueAsNumber
    wavesurfer.zoom(minPxPerSec)
  })

  document.querySelector('button').addEventListener('click', () => {
    wavesurfer.playPause()
  })
})


================================================
FILE: examples/fm-synth.js
================================================
// A two-operator FM synth with a real-time waveform

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  cursorColor: 'transparent',
  barWidth: 2,
  interact: false,
})

const audioContext = new AudioContext()

// Create an analyser node
const analyser = audioContext.createAnalyser()
analyser.fftSize = 512 * 2
analyser.connect(audioContext.destination)
const dataArray = new Float32Array(analyser.frequencyBinCount)

function createVoice() {
  // Carrier oscillator
  const carrierOsc = audioContext.createOscillator()
  carrierOsc.type = 'sine'

  // Modulator oscillator
  const modulatorOsc = audioContext.createOscillator()
  modulatorOsc.type = 'sine'

  // Modulation depth
  const modulationGain = audioContext.createGain()

  // Connect the modulator to the carrier frequency
  modulatorOsc.connect(modulationGain)
  modulationGain.connect(carrierOsc.frequency)

  // Create an output gain
  const outputGain = audioContext.createGain()
  outputGain.gain.value = 0

  // Connect carrier oscillator to output
  carrierOsc.connect(outputGain)

  // Connect output to analyser
  outputGain.connect(analyser)

  // Start oscillators
  carrierOsc.start()
  modulatorOsc.start()

  return {
    carrierOsc,
    modulatorOsc,
    modulationGain,
    outputGain,
  }
}

function playNote(frequency, modulationFrequency, modulationDepth, duration) {
  const voice = createVoice()
  const { carrierOsc, modulatorOsc, modulationGain, outputGain } = voice

  carrierOsc.frequency.value = frequency
  modulatorOsc.frequency.value = modulationFrequency
  modulationGain.gain.value = modulationDepth

  outputGain.gain.setValueAtTime(0.00001, audioContext.currentTime)
  outputGain.gain.exponentialRampToValueAtTime(1, audioContext.currentTime + duration / 1000)

  return voice
}

function releaseNote(voice, duration) {
  const { carrierOsc, modulatorOsc, modulationGain, outputGain } = voice
  outputGain.gain.cancelScheduledValues(audioContext.currentTime)
  outputGain.gain.setValueAtTime(1, audioContext.currentTime)
  outputGain.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + duration / 1000)

  setTimeout(() => {
    carrierOsc.stop()
    modulatorOsc.stop()
    carrierOsc.disconnect()
    modulatorOsc.disconnect()
    modulationGain.disconnect()
    outputGain.disconnect()
    voice.carrierOsc = null
    voice.modulatorOsc = null
    voice.modulationGain = null
    voice.outputGain = null
  }, duration + 100)
}

function createPianoRoll() {
  const baseFrequency = 110
  const numRows = 4
  const numCols = 10

  const noteFrequency = (row, col) => {
    // The top row is the bass
    // The lower rows represent the notes of a major third chord
    // Columns represent the notes of a C major scale (there are 10 columns and 4 rows)
    const chord = [-8, 0, 4, 7]
    const scale = [0, 2, 4, 5, 7, 9, 11, 12, 14, 16]
    const note = chord[row] + scale[col]
    return baseFrequency * Math.pow(2, note / 12)
  }

  const pianoRoll = document.getElementById('pianoRoll')
  const qwerty = '1234567890qwertyuiopasdfghjkl;zxcvbnm,./'
  const capsQwerty = '!@#$%^&*()QWERTYUIOPASDFGHJKL:ZXCVBNM<>?'

  const onKeyDown = (freq) => {
    const modulationIndex = parseFloat(document.getElementById('modulationIndex').value)
    const modulationDepth = parseFloat(document.getElementById('modulationDepth').value)
    const duration = parseFloat(document.getElementById('duration').value)
    return playNote(freq, freq * modulationIndex, modulationDepth, duration)
  }

  const onKeyUp = (voice) => {
    const duration = parseFloat(document.getElementById('duration').value)
    releaseNote(voice, duration)
  }

  const createButton = (row, col) => {
    const button = document.createElement('button')
    const key = qwerty[(row * numCols + col) % qwerty.length]
    const capsKey = capsQwerty[(row * numCols + col) % capsQwerty.length]
    const frequency = noteFrequency(row, col)
    let note = null

    button.textContent = key
    pianoRoll.appendChild(button)

    // Mouse
    button.addEventListener('mousedown', (e) => {
      note = onKeyDown(frequency * (e.shiftKey ? numRows : 1))
    })
    button.addEventListener('mouseup', () => {
      if (note) {
        onKeyUp(note)
        note = null
      }
    })

    // Keyboard
    document.addEventListener('keydown', (e) => {
      if (e.key === key || e.key === capsKey) {
        button.className = 'active'
        if (!note) {
          note = onKeyDown(frequency * (e.shiftKey ? numRows : 1))
        }
      }
    })
    document.addEventListener('keyup', (e) => {
      if (e.key === key || e.key === capsKey) {
        button.className = ''
        if (note) {
          onKeyUp(note)
          note = null
        }
      }
    })
  }

  for (let row = 0; row < numRows; row++) {
    for (let col = 0; col < numCols; col++) {
      createButton(row, col)
    }
  }

  const buttons = document.querySelectorAll('button')
  document.addEventListener('keydown', (e) => {
    if (e.shiftKey) {
      Array.from(buttons).forEach((button, index) => {
        button.textContent = capsQwerty[index]
      })
    }
  })
  document.addEventListener('keyup', (e) => {
    if (!e.shiftKey) {
      Array.from(buttons).forEach((button, index) => {
        button.textContent = qwerty[index]
      })
    }
  })
}

function randomizeFmParams() {
  document.getElementById('modulationIndex').value = Math.random() * 10
  document.getElementById('modulationDepth').value = Math.random() * 200
  document.getElementById('duration').value = Math.random() * 1000
}

// Draw the waveform
function drawWaveform() {
  // Get the waveform data from the analyser
  analyser.getFloatTimeDomainData(dataArray)
  const duration = document.getElementById('duration').valueAsNumber
  wavesurfer && wavesurfer.load('', [dataArray], duration)
}

function animate() {
  requestAnimationFrame(animate)
  drawWaveform()
}

createPianoRoll()
animate()
randomizeFmParams()

/*
<html>
  <style>
    label {
      display: inline-block;
      width: 150px;
    }
    #pianoRoll {
      margin-top: 1em;
      width: 100%;
      display: grid;
      grid-template-columns: repeat(10, 6vw);
      grid-template-rows: repeat(5, 6vw);
      gap: 5px;
      user-select: none;
    }
    button {
      width: 100%;
      height: 100%;
      border: 1px solid #aaa;
      background-color: #fff;
      cursor: pointer;
    }
    button:nth-child(n + 11):nth-child(-n + 20) {
      margin-left: 5px;
    }
    button:nth-child(n + 21):nth-child(-n + 30) {
      margin-left: 10px;
    }
    button:nth-child(n + 31):nth-child(-n + 40) {
      margin-left: 15px;
    }
    button.active,
    button:active {
      background-color: #00f;
      color: #fff;
    }
  </style>
  <div>
    <label>Modulation index:</label>
    <input type="range" min="0.5" max="10" value="2" step="0.5" id="modulationIndex">
  </div>
  <div>
    <label>Modulation depth:</label>
    <input type="range" min="1" max="200" value="50" step="1" id="modulationDepth">
  </div>
  <div>
    <label>Attack/release:</label>
    <input type="range" min="100" max="1000" value="100" step="10" id="duration">
  </div>
  <p>
    Hold Shift to play the notes one octave higher
  </p>
  <div id="pianoRoll"></div>
  <div id="waveform"></div>
</html>
*/


================================================
FILE: examples/gradient.js
================================================
// Fancy gradients

import WaveSurfer from 'wavesurfer.js'

// Create a canvas gradient
const ctx = document.createElement('canvas').getContext('2d')
const gradient = ctx.createLinearGradient(0, 0, 0, 150)
gradient.addColorStop(0, 'rgb(200, 0, 200)')
gradient.addColorStop(0.7, 'rgb(100, 0, 100)')
gradient.addColorStop(1, 'rgb(0, 0, 0)')

// Default style with a gradient
WaveSurfer.create({
  container: document.body,
  waveColor: gradient,
  progressColor: 'rgba(0, 0, 100, 0.5)',
  url: '/examples/audio/audio.wav',
})

// SoundCloud-style bars
WaveSurfer.create({
  container: document.body,
  waveColor: gradient,
  barWidth: 2,
  progressColor: 'rgba(0, 0, 100, 0.5)',
  url: '/examples/audio/audio.wav',
})


================================================
FILE: examples/hover.js
================================================
// Hover plugin

import WaveSurfer from 'wavesurfer.js'
import Hover from 'wavesurfer.js/dist/plugins/hover.esm.js'

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  plugins: [
    Hover.create({
      lineColor: '#ff0000',
      lineWidth: 2,
      labelBackground: '#555',
      labelColor: '#fff',
      labelSize: '11px',
      labelPreferLeft: false,
    }),
  ],
})

ws.on('interaction', () => {
  ws.play()
})

/*
<html>
  <style>
    #waveform ::part(hover-label):before {
      content: '⏱️ ';
    }
  </style>

  <div id="waveform"></div>

  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_hover.HoverPlugin">Hover plugin docs</a>
  </p>
</html>
*/


================================================
FILE: examples/minimap.js
================================================
// Minimap plugin

import WaveSurfer from 'wavesurfer.js'
import Minimap from 'wavesurfer.js/dist/plugins/minimap.esm.js'

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  minPxPerSec: 100,
  hideScrollbar: true,
  autoCenter: false,
  plugins: [
    // Register the plugin
    Minimap.create({
      height: 20,
      waveColor: '#ddd',
      progressColor: '#999',
      // the Minimap takes all the same options as the WaveSurfer itself
    }),
  ],
})

ws.on('interaction', () => {
  ws.play()
})

/*
<html>
  <div id="waveform"></div>
  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_minimap.MinimapPlugin">Minimap plugin docs</a>
  </p>
</html>
*/


================================================
FILE: examples/multitrack.js
================================================
/**
 * Multi-track mixer
 *
 * @see https://github.com/katspaugh/wavesurfer-multitrack
 */

/*
<html>
  <script src="https://unpkg.com/wavesurfer-multitrack/dist/multitrack.min.js"></script>

  <label>
    Zoom: <input type="range" min="10" max="100" value="10" />
  </label>

  <div style="margin: 2em 0">
    <button id="play">Play</button>
    <button id="forward">Forward 30s</button>
    <button id="backward">Back 30s</button>
  </div>

  <div id="container" style="background: #2d2d2d; color: #fff"></div>
</html>
*/

// Call Multitrack.create to initialize a multitrack mixer
// Pass a tracks array and WaveSurfer options with a container element
const multitrack = Multitrack.create(
  [
    {
      id: 0,
    },
    {
      id: 1,
      draggable: false,
      startPosition: 14, // start time relative to the entire multitrack
      url: '/examples/audio/librivox.mp3',
      envelope: [
        { time: 2, volume: 0.5 },
        { time: 10, volume: 0.8 },
        { time: 255, volume: 0.8 },
        { time: 264, volume: 0 },
      ],
      volume: 0.95,
      options: {
        waveColor: 'hsl(46, 87%, 49%)',
        progressColor: 'hsl(46, 87%, 20%)',
      },
      intro: {
        endTime: 16,
        label: 'Intro',
        color: '#FFE56E',
      },
      markers: [
        {
          time: 21,
          label: 'M1',
          color: 'hsla(600, 100%, 30%, 0.5)',
        },
        {
          time: 22.7,
          label: 'M2',
          color: 'hsla(400, 100%, 30%, 0.5)',
        },
        {
          time: 24,
          label: 'M3',
          color: 'hsla(200, 50%, 70%, 0.5)',
        },
        {
          time: 27,
          label: 'M4',
          color: 'hsla(200, 50%, 70%, 0.5)',
        },
      ],
      // peaks: [ [ 0, 0, 2.567, -2.454, 10.5645 ] ], // optional pre-generated peaks
    },
    {
      id: 2,
      draggable: true,
      startPosition: 1,
      startCue: 2.1,
      endCue: 20,
      fadeInEnd: 8,
      fadeOutStart: 14,
      envelope: true,
      volume: 0.8,
      options: {
        waveColor: 'hsl(161, 87%, 49%)',
        progressColor: 'hsl(161, 87%, 20%)',
      },
      url: '/examples/audio/audio.wav',
    },
    {
      id: 3,
      draggable: true,
      startPosition: 290,
      volume: 0.8,
      options: {
        waveColor: 'hsl(161, 87%, 49%)',
        progressColor: 'hsl(161, 87%, 20%)',
      },
      url: '/examples/audio/demo.wav',
    },
  ],
  {
    container: document.querySelector('#container'), // required!
    minPxPerSec: 10, // zoom level
    rightButtonDrag: false, // set to true to drag with right mouse button
    cursorWidth: 2,
    cursorColor: '#D72F21',
    trackBackground: '#2D2D2D',
    trackBorderColor: '#7C7C7C',
    dragBounds: true,
    envelopeOptions: {
      lineColor: 'rgba(255, 0, 0, 0.7)',
      lineWidth: 4,
      dragPointSize: window.innerWidth < 600 ? 20 : 10,
      dragPointFill: 'rgba(255, 255, 255, 0.8)',
      dragPointStroke: 'rgba(255, 255, 255, 0.3)',
    },
  },
)

// Events
multitrack.on('start-position-change', ({ id, startPosition }) => {
  console.log(`Track ${id} start position updated to ${startPosition}`)
})

multitrack.on('start-cue-change', ({ id, startCue }) => {
  console.log(`Track ${id} start cue updated to ${startCue}`)
})

multitrack.on('end-cue-change', ({ id, endCue }) => {
  console.log(`Track ${id} end cue updated to ${endCue}`)
})

multitrack.on('volume-change', ({ id, volume }) => {
  console.log(`Track ${id} volume updated to ${volume}`)
})

multitrack.on('fade-in-change', ({ id, fadeInEnd }) => {
  console.log(`Track ${id} fade-in updated to ${fadeInEnd}`)
})

multitrack.on('fade-out-change', ({ id, fadeOutStart }) => {
  console.log(`Track ${id} fade-out updated to ${fadeOutStart}`)
})

multitrack.on('intro-end-change', ({ id, endTime }) => {
  console.log(`Track ${id} intro end updated to ${endTime}`)
})

multitrack.on('envelope-points-change', ({ id, points }) => {
  console.log(`Track ${id} envelope points updated to`, points)
})

multitrack.on('drop', ({ id }) => {
  multitrack.addTrack({
    id,
    url: '/examples/audio/demo.wav',
    startPosition: 0,
    draggable: true,
    options: {
      waveColor: 'hsl(25, 87%, 49%)',
      progressColor: 'hsl(25, 87%, 20%)',
    },
  })
})

// Play/pause button
const button = document.querySelector('#play')
button.disabled = true
multitrack.once('canplay', () => {
  button.disabled = false
  button.onclick = () => {
    multitrack.isPlaying() ? multitrack.pause() : multitrack.play()
    button.textContent = multitrack.isPlaying() ? 'Pause' : 'Play'
  }
})

// Forward/back buttons
const forward = document.querySelector('#forward')
forward.onclick = () => {
  multitrack.setTime(multitrack.getCurrentTime() + 30)
}
const backward = document.querySelector('#backward')
backward.onclick = () => {
  multitrack.setTime(multitrack.getCurrentTime() - 30)
}

// Zoom
const slider = document.querySelector('input[type="range"]')
slider.oninput = () => {
  multitrack.zoom(slider.valueAsNumber)
}

// Destroy all wavesurfer instances on unmount
// This should be called before calling initMultiTrack again to properly clean up
window.onbeforeunload = () => {
  multitrack.destroy()
}

// Set sinkId
multitrack.once('canplay', async () => {
  await multitrack.setSinkId('default')
  console.log('Set sinkId to default')
})


================================================
FILE: examples/phase-vocoder/index.js
================================================
// WebAudio speed control with pitch preservation

import WaveSurfer from 'wavesurfer.js'

// Init wavesurfer
const wavesurfer = WaveSurfer.create({
  backend: 'WebAudio',
  container: document.body,
  waveColor: 'violet',
  progressColor: 'purple',
  url: '/examples/audio/librivox.mp3',
})

// Wait for the audio to be ready
wavesurfer.on('ready', async () => {
  const webAudioPlayer = wavesurfer.getMediaElement()
  const gainNode = webAudioPlayer.getGainNode()
  const audioContext = gainNode.context

  // Load the phase vocoder audio worklet
  await audioContext.audioWorklet.addModule('/examples/phase-vocoder/phase-vocoder.min.js')
  const phaseVocoderNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor')

  // Connect the worklet to the wavesurfer audio
  gainNode.disconnect()
  gainNode.connect(phaseVocoderNode)
  phaseVocoderNode.connect(audioContext.destination)

  // Speed slider
  document.querySelector('input[type="range"]').addEventListener('input', (e) => {
    const speed = e.target.valueAsNumber
    document.querySelector('#rate').textContent = speed.toFixed(2)
    wavesurfer.setPlaybackRate(speed)
    const pitchFactorParam = phaseVocoderNode.parameters.get('pitchFactor')
    pitchFactorParam.value = 1 / speed
  })

  // Play/pause button
  document.querySelector('button').addEventListener('click', () => {
    wavesurfer.playPause()
  })
})

/*
<html>
  <div style="display: flex; margin: 1rem 0; gap: 1rem;">
    <button>
      Play/pause
    </button>

    <label>
      Playback rate: <span id="rate">1.00</span>x
    </label>

    <label>
      0.1x <input type="range" min="0.1" max="4" step="0.1" value="1" /> 4x
    </label>
  </div>

  <p>
    📖 Based on <a href="https://github.com/olvb/phaze" target="_top">github.com/olvb/phaze</a>
  </p>
</html>
*/


================================================
FILE: examples/pitch-worker.js
================================================
import Pitchfinder from 'https://esm.sh/pitchfinder'

onmessage = (e) => {
  const { peaks, sampleRate = 8000, algo = 'AMDF' } = e.data
  const detectPitch = Pitchfinder[algo]({ sampleRate })
  const duration = peaks.length / sampleRate
  const bpm = peaks.length / duration / 60

  const frequencies = Pitchfinder.frequencies(detectPitch, peaks, {
    tempo: bpm,
    quantization: bpm,
  })

  // Find the baseline frequency (the value that appears most often)
  const frequencyMap = {}
  let maxAmount = 0
  let baseFrequency = 0
  frequencies.forEach((frequency) => {
    if (!frequency) return
    const tolerance = 10
    frequency = Math.round(frequency * tolerance) / tolerance
    if (!frequencyMap[frequency]) frequencyMap[frequency] = 0
    frequencyMap[frequency] += 1
    if (frequencyMap[frequency] > maxAmount) {
      maxAmount = frequencyMap[frequency]
      baseFrequency = frequency
    }
  })

  postMessage({
    frequencies,
    baseFrequency,
  })
}


================================================
FILE: examples/pitch.js
================================================
import WaveSurfer from 'wavesurfer.js'

const pitchWorker = new Worker('/examples/pitch-worker.js', { type: 'module' })

const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgba(200, 200, 200, 0.5)',
  progressColor: 'rgba(100, 100, 100, 0.5)',
  url: '/examples/audio/librivox.mp3',
  minPxPerSec: 200,
  sampleRate: 11025,
})

// Pitch detection
wavesurfer.on('decode', () => {
  const peaks = wavesurfer.getDecodedData().getChannelData(0)
  pitchWorker.postMessage({ peaks, sampleRate: wavesurfer.options.sampleRate })
})

// When the worker sends back pitch data, update the UI
pitchWorker.onmessage = (e) => {
  const { frequencies, baseFrequency } = e.data

  // Render the frequencies on a canvas
  const pitchUpColor = '#385587'
  const pitchDownColor = '#C26351'
  const height = 100

  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  canvas.width = frequencies.length
  canvas.height = height
  canvas.style.width = '100%'
  canvas.style.height = '100%'

  // Each frequency is a point whose Y position is the frequency and X position is the time
  const pointSize = devicePixelRatio
  let prevY = 0
  frequencies.forEach((frequency, index) => {
    if (!frequency) return
    const y = Math.round(height - (frequency / (baseFrequency * 2)) * height)
    ctx.fillStyle = y > prevY ? pitchDownColor : pitchUpColor
    ctx.fillRect(index, y, pointSize, pointSize)
    prevY = y
  })

  // Add the canvas to the waveform container
  wavesurfer.renderer.getWrapper().appendChild(canvas)
  // Remove the canvas when a new audio is loaded
  wavesurfer.once('load', () => canvas.remove())
}

// Play on click
wavesurfer.on('interaction', () => {
  if (!wavesurfer.isPlaying()) wavesurfer.play()
})

// Drag'n'drop
{
  const dropArea = document.querySelector('#drop')
  dropArea.ondragenter = (e) => {
    e.preventDefault()
    e.target.classList.add('over')
  }
  dropArea.ondragleave = (e) => {
    e.preventDefault()
    e.target.classList.remove('over')
  }
  dropArea.ondragover = (e) => {
    e.preventDefault()
  }
  dropArea.ondrop = (e) => {
    e.preventDefault()
    e.target.classList.remove('over')

    // Read the audio file
    const reader = new FileReader()
    reader.onload = (event) => {
      wavesurfer.load(event.target.result)
    }
    reader.readAsDataURL(e.dataTransfer.files[0])

    // Write the name of the file into the drop area
    dropArea.textContent = e.dataTransfer.files[0].name
    wavesurfer.empty()
  }
  document.body.ondrop = (e) => {
    e.preventDefault()
  }
}

/*
<html>
<style>
#drop {
  height: 128px;
  border: 4px dashed #999;
  margin: 2em 0;
  text-align:center;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
#drop.over {
  border-color: #333;
}
</style>

<p align="right">Audio from <a href="https://librivox.org/">LibriVox</a></p>
<div id="waveform"></div>
<div id="drop">Drag-n-drop your own audio file</div>
</html>
*/


================================================
FILE: examples/predecoded.js
================================================
// With pre-decoded audio data

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  barWidth: 10,
  barRadius: 10,
  barGap: 2,
  url: '/examples/audio/demo.wav',
  peaks: [
    [
      0, 0.0023595101665705442, 0.012107174843549728, 0.005919494666159153, -0.31324470043182373, 0.1511787623167038,
      0.2473851442337036, 0.11443428695201874, -0.036057762801647186, -0.0968964695930481, -0.03033737652003765,
      0.10682467371225357, 0.23974689841270447, 0.013210971839725971, -0.12377244979143143, 0.046145666390657425,
      -0.015757400542497635, 0.10884027928113937, 0.06681904196739197, 0.09432944655418396, -0.17105795443058014,
      -0.023439358919858932, -0.10380347073078156, 0.0034454423002898693, 0.08061369508504868, 0.026129156351089478,
      0.18730352818965912, 0.020447958260774612, -0.15030759572982788, 0.05689578503370285, -0.0009095853311009705,
      0.2749626338481903, 0.2565386891365051, 0.07571295648813248, 0.10791446268558502, -0.06575305759906769,
      0.15336275100708008, 0.07056761533021927, 0.03287476301193237, -0.09044631570577621, 0.01777501218020916,
      -0.04906218498945236, -0.04756792634725571, -0.006875281687825918, 0.04520256072282791, -0.02362387254834175,
      -0.0668797641992569, 0.12266506254673004, -0.10895221680402756, 0.03791835159063339, -0.0195105392485857,
      -0.031097881495952606, 0.04252675920724869, -0.09187793731689453, 0.0829525887966156, -0.003812957089394331,
      0.0431736595928669, 0.07634212076663971, -0.05335947126150131, 0.0345163568854332, -0.049201950430870056,
      0.02300390601158142, 0.007677287794649601, 0.015354577451944351, 0.007677287794649601, 0.007677288725972176,
    ],
  ],
  duration: 22,
})

wavesurfer.on('interaction', () => {
  wavesurfer.play()
})

wavesurfer.on('finish', () => {
  wavesurfer.setTime(0)
})


================================================
FILE: examples/react-global-player.js
================================================
// React example

/*
  <html>
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </html>
*/

// Import React hooks
const { useRef, useState, useEffect, useCallback, memo } = React

// Import WaveSurfer
import WaveSurfer from 'wavesurfer.js'

// WaveSurfer hook
const useWavesurfer = (containerRef, options) => {
  const [wavesurfer, setWavesurfer] = useState(null)

  // Initialize wavesurfer when the container mounts
  // or any of the props change
  useEffect(() => {
    if (!containerRef.current) return

    const ws = WaveSurfer.create({
      ...options,
      container: containerRef.current,
    })

    setWavesurfer(ws)

    return () => {
      ws.destroy()
    }
  }, [options, containerRef])

  return wavesurfer
}

// Create a React component that will render wavesurfer.
// Props are wavesurfer options.
const WaveSurferPlayer = memo((props) => {
  const containerRef = useRef()
  const [isPlaying, setIsPlaying] = useState(false)
  const wavesurfer = useWavesurfer(containerRef, props)
  const { onPlay, onReady } = props

  // On play button click
  const onPlayClick = useCallback(() => {
    wavesurfer.playPause()
  }, [wavesurfer])

  // Initialize wavesurfer when the container mounts
  // or any of the props change
  useEffect(() => {
    if (!wavesurfer) return

    const getPlayerParams = () => ({
      media: wavesurfer.getMediaElement(),
      peaks: wavesurfer.exportPeaks(),
    })

    const subscriptions = [
      wavesurfer.on('ready', () => {
        onReady && onReady(getPlayerParams())

        setIsPlaying(wavesurfer.isPlaying())
      }),
      wavesurfer.on('play', () => {
        onPlay &&
          onPlay((prev) => {
            const newParams = getPlayerParams()
            if (!prev || prev.media !== newParams.media) {
              if (prev) {
                prev.media.pause()
                prev.media.currentTime = 0
              }
              return newParams
            }
            return prev
          })

        setIsPlaying(true)
      }),
      wavesurfer.on('pause', () => setIsPlaying(false)),
    ]

    return () => {
      subscriptions.forEach((unsub) => unsub())
    }
  }, [wavesurfer, onPlay, onReady])

  return (
    <div style={{ display: 'flex', gap: '1em', marginBottom: '1em' }}>
      <button onClick={onPlayClick}>{isPlaying ? '⏸️' : '▶️'}</button>

      <div ref={containerRef} style={{ minWidth: '200px' }} />
    </div>
  )
})

const Playlist = memo(({ urls, setCurrentPlayer }) => {
  return urls.map((url, index) => (
    <WaveSurferPlayer
      key={url}
      height={100}
      waveColor="rgb(200, 0, 200)"
      progressColor="rgb(100, 0, 100)"
      url={url}
      onPlay={setCurrentPlayer}
      onReady={index === 0 ? setCurrentPlayer : undefined}
    />
  ))
})

const audioUrls = ['/examples/audio/audio.wav', '/examples/audio/demo.wav', '/examples/audio/stereo.mp3']

const App = () => {
  const [currentPlayer, setCurrentPlayer] = useState()

  return (
    <>
      <p>Playlist</p>
      <Playlist urls={audioUrls} setCurrentPlayer={setCurrentPlayer} />

      <p>Global player</p>
      {currentPlayer && (
        <WaveSurferPlayer
          height={50}
          waveColor="blue"
          progressColor="purple"
          media={currentPlayer.media}
          peaks={currentPlayer.peaks}
        />
      )}
    </>
  )
}

// Create a React root and render the app
const root = ReactDOM.createRoot(document.body)
root.render(<App />)


================================================
FILE: examples/react.js
================================================
// React example
// See https://github.com/katspaugh/wavesurfer-react

import * as React from 'react'
const { useMemo, useState, useCallback, useRef } = React
import { createRoot } from 'react-dom/client'
import { useWavesurfer } from '@wavesurfer/react'
import Timeline from 'wavesurfer.js/dist/plugins/timeline.esm.js'

const audioUrls = [
  '/examples/audio/audio.wav',
  '/examples/audio/stereo.mp3',
  '/examples/audio/mono.mp3',
  '/examples/audio/librivox.mp3',
]

const formatTime = (seconds) => [seconds / 60, seconds % 60].map((v) => `0${Math.floor(v)}`.slice(-2)).join(':')

// A React component that will render wavesurfer
const App = () => {
  const containerRef = useRef(null)
  const [urlIndex, setUrlIndex] = useState(0)

  const { wavesurfer, isPlaying, currentTime } = useWavesurfer({
    container: containerRef,
    height: 100,
    waveColor: 'rgb(200, 0, 200)',
    progressColor: 'rgb(100, 0, 100)',
    url: audioUrls[urlIndex],
    plugins: useMemo(() => [Timeline.create()], []),
  })

  const onUrlChange = useCallback(() => {
    setUrlIndex((index) => (index + 1) % audioUrls.length)
  }, [])

  const onPlayPause = useCallback(() => {
    wavesurfer && wavesurfer.playPause()
  }, [wavesurfer])

  return (
    <>
      <div ref={containerRef} />

      <p>Current audio: {audioUrls[urlIndex]}</p>

      <p>Current time: {formatTime(currentTime)}</p>

      <div style={{ margin: '1em 0', display: 'flex', gap: '1em' }}>
        <button onClick={onUrlChange}>Change audio</button>

        <button onClick={onPlayPause} style={{ minWidth: '5em' }}>
          {isPlaying ? 'Pause' : 'Play'}
        </button>
      </div>
    </>
  )
}

// Create a React root and render the app
const root = createRoot(document.body)
root.render(<App />)

/*
  <html>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script type="importmap">
      {
        "imports": {
          "react": "https://esm.sh/react",
          "react/jsx-runtime": "https://esm.sh/react/jsx-runtime",
          "react-dom/client": "https://esm.sh/react-dom/client",
          "wavesurfer.js": "../dist/wavesurfer.esm.js",
          "wavesurfer.js/dist": "../dist",
          "@wavesurfer/react": "https://unpkg.com/@wavesurfer/react"
        }
      }
    </script>
  </html>
*/


================================================
FILE: examples/record-sync.js
================================================
// Record plugin

import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm.js'

let wavesurfer, record
let scrollingWaveform = false
let continuousWaveform = true

const wavesurfer2 = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',

  // Set a bar width
  barWidth: 2,
  // Optionally, specify the spacing between bars
  barGap: 1,
  // And the bar radius
  barRadius: 2,
})

wavesurfer2.on('ready', function () {
  const createWaveSurfer = () => {
    // Destroy the previous wavesurfer instance
    if (wavesurfer) {
      wavesurfer.destroy()
    }

    // Create a new Wavesurfer instance
    wavesurfer = WaveSurfer.create({
      container: '#mic',
      waveColor: 'rgb(200, 0, 200)',
      progressColor: 'rgb(100, 0, 100)',
    })

    // Initialize the Record plugin
    record = wavesurfer.registerPlugin(
      RecordPlugin.create({
        renderRecordedAudio: false,
        scrollingWaveform,
        continuousWaveform,
        continuousWaveformDuration: wavesurfer2.getDuration(),
      }),
    )

    // Render recorded audio
    record.on('record-end', (blob) => {
      const container = document.querySelector('#recordings')
      const recordedUrl = URL.createObjectURL(blob)

      // Create wavesurfer from the recorded audio
      const wavesurfer = WaveSurfer.create({
        container,
        waveColor: 'rgb(200, 100, 0)',
        progressColor: 'rgb(100, 50, 0)',
        url: recordedUrl,
      })

      // Play button
      const button = container.appendChild(document.createElement('button'))
      button.textContent = 'Play'
      button.onclick = () => wavesurfer.playPause()
      wavesurfer.on('pause', () => (button.textContent = 'Play'))
      wavesurfer.on('play', () => (button.textContent = 'Pause'))

      // Download link
      const link = container.appendChild(document.createElement('a'))
      Object.assign(link, {
        href: recordedUrl,
        download: 'recording.' + blob.type.split(';')[0].split('/')[1] || 'webm',
        textContent: 'Download recording',
      })
    })
    pauseButton.style.display = 'none'
    recButton.textContent = 'Record'

    record.on('record-progress', (time) => {
      updateProgress(time)
    })
  }

  const progress = document.querySelector('#progress')
  const updateProgress = (time) => {
    // time will be in milliseconds, convert it to mm:ss format
    const formattedTime = [
      Math.floor((time % 3600000) / 60000), // minutes
      Math.floor((time % 60000) / 1000), // seconds
    ]
      .map((v) => (v < 10 ? '0' + v : v))
      .join(':')
    progress.textContent = formattedTime
  }

  const pauseButton = document.querySelector('#pause')
  pauseButton.onclick = () => {
    if (record.isPaused()) {
      wavesurfer2.play()
      record.resumeRecording()
      pauseButton.textContent = 'Pause'
      return
    }

    wavesurfer2.pause()
    record.pauseRecording()
    pauseButton.textContent = 'Resume'
  }

  const micSelect = document.querySelector('#mic-select')
  {
    // Mic selection
    RecordPlugin.getAvailableAudioDevices().then((devices) => {
      devices.forEach((device) => {
        const option = document.createElement('option')
        option.value = device.deviceId
        option.text = device.label || device.deviceId
        micSelect.appendChild(option)
      })
    })
  }
  // Record button
  const recButton = document.querySelector('#record')

  recButton.onclick = () => {
    wavesurfer2.play()
    if (record.isRecording() || record.isPaused()) {
      wavesurfer2.pause()
      record.stopRecording()
      recButton.textContent = 'Record'
      pauseButton.style.display = 'none'
      return
    }

    recButton.disabled = true

    // reset the wavesurfer instance

    // get selected device
    const deviceId = micSelect.value
    record.startRecording({ deviceId }).then(() => {
      recButton.textContent = 'Stop'
      recButton.disabled = false
      pauseButton.style.display = 'inline'
    })
  }

  document.querySelector('#scrollingWaveform').onclick = (e) => {
    scrollingWaveform = e.target.checked
    if (continuousWaveform && scrollingWaveform) {
      continuousWaveform = false
      document.querySelector('#continuousWaveform').checked = false
    }
    createWaveSurfer()
  }

  document.querySelector('#continuousWaveform').onclick = (e) => {
    continuousWaveform = e.target.checked
    if (continuousWaveform && scrollingWaveform) {
      scrollingWaveform = false
      document.querySelector('#scrollingWaveform').checked = false
    }
    createWaveSurfer()
  }

  createWaveSurfer()
})

/*
<html>
  <h1 style="margin-top: 0">Press Record to start recording 🎙️</h1>

  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_record.RecordPlugin">Record plugin docs</a>
  </p>

  <button id="record">Record</button>
  <button id="pause" style="display: none;">Pause</button>

  <select id="mic-select">
    <option value="" hidden>Select mic</option>
  </select>

  <label><input type="checkbox" id="scrollingWaveform" /> Scrolling waveform</label>

  <label><input type="checkbox" id="continuousWaveform" checked="checked" /> Continuous waveform</label>

  <p id="progress">00:00</p>

  <div id="mic" style="border-radius: 4px; margin-top: 1rem"></div>

  <div id="recordings" style="margin: 1rem 0"></div>

  <style>
    button {
      min-width: 5rem;
      margin: 1rem 1rem 1rem 0;
    }
  </style>
</html>
*/


================================================
FILE: examples/record.js
================================================
// Record plugin

import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm.js'

let wavesurfer, record
let scrollingWaveform = false
let continuousWaveform = true

const createWaveSurfer = () => {
  // Destroy the previous wavesurfer instance
  if (wavesurfer) {
    wavesurfer.destroy()
  }

  // Create a new Wavesurfer instance
  wavesurfer = WaveSurfer.create({
    container: '#mic',
    waveColor: 'rgb(200, 0, 200)',
    progressColor: 'rgb(100, 0, 100)',
  })

  // Initialize the Record plugin
  record = wavesurfer.registerPlugin(
    RecordPlugin.create({
      renderRecordedAudio: false,
      scrollingWaveform,
      continuousWaveform,
      continuousWaveformDuration: 30, // optional
    }),
  )

  // Render recorded audio
  record.on('record-end', (blob) => {
    const container = document.querySelector('#recordings')
    const recordedUrl = URL.createObjectURL(blob)

    // Create wavesurfer from the recorded audio
    const wavesurfer = WaveSurfer.create({
      container,
      waveColor: 'rgb(200, 100, 0)',
      progressColor: 'rgb(100, 50, 0)',
      url: recordedUrl,
    })

    // Play button
    const button = container.appendChild(document.createElement('button'))
    button.textContent = 'Play'
    button.onclick = () => wavesurfer.playPause()
    wavesurfer.on('pause', () => (button.textContent = 'Play'))
    wavesurfer.on('play', () => (button.textContent = 'Pause'))

    // Download link
    const link = container.appendChild(document.createElement('a'))
    Object.assign(link, {
      href: recordedUrl,
      download: 'recording.' + blob.type.split(';')[0].split('/')[1] || 'webm',
      textContent: 'Download recording',
    })
  })
  pauseButton.style.display = 'none'
  recButton.textContent = 'Record'

  record.on('record-progress', (time) => {
    updateProgress(time)
  })
}

const progress = document.querySelector('#progress')
const updateProgress = (time) => {
  // time will be in milliseconds, convert it to mm:ss format
  const formattedTime = [
    Math.floor((time % 3600000) / 60000), // minutes
    Math.floor((time % 60000) / 1000), // seconds
  ]
    .map((v) => (v < 10 ? '0' + v : v))
    .join(':')
  progress.textContent = formattedTime
}

const pauseButton = document.querySelector('#pause')
pauseButton.onclick = () => {
  if (record.isPaused()) {
    record.resumeRecording()
    pauseButton.textContent = 'Pause'
    return
  }

  record.pauseRecording()
  pauseButton.textContent = 'Resume'
}

const micSelect = document.querySelector('#mic-select')
{
  // Mic selection
  RecordPlugin.getAvailableAudioDevices().then((devices) => {
    devices.forEach((device) => {
      const option = document.createElement('option')
      option.value = device.deviceId
      option.text = device.label || device.deviceId
      micSelect.appendChild(option)
    })
  })
}
// Record button
const recButton = document.querySelector('#record')

recButton.onclick = () => {
  if (record.isRecording() || record.isPaused()) {
    record.stopRecording()
    recButton.textContent = 'Record'
    pauseButton.style.display = 'none'
    return
  }

  recButton.disabled = true

  // reset the wavesurfer instance

  // get selected device
  const deviceId = micSelect.value
  record.startRecording({ deviceId }).then(() => {
    recButton.textContent = 'Stop'
    recButton.disabled = false
    pauseButton.style.display = 'inline'
  })
}

document.querySelector('#scrollingWaveform').onclick = (e) => {
  scrollingWaveform = e.target.checked
  if (continuousWaveform && scrollingWaveform) {
    continuousWaveform = false
    document.querySelector('#continuousWaveform').checked = false
  }
  createWaveSurfer()
}

document.querySelector('#continuousWaveform').onclick = (e) => {
  continuousWaveform = e.target.checked
  if (continuousWaveform && scrollingWaveform) {
    scrollingWaveform = false
    document.querySelector('#scrollingWaveform').checked = false
  }
  createWaveSurfer()
}

createWaveSurfer()

/*
<html>
  <h1 style="margin-top: 0">Press Record to start recording 🎙️</h1>

  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_record.RecordPlugin">Record plugin docs</a>
  </p>

  <button id="record">Record</button>
  <button id="pause" style="display: none;">Pause</button>

  <select id="mic-select">
    <option value="" hidden>Select mic</option>
  </select>

  <label><input type="checkbox" id="scrollingWaveform" /> Scrolling waveform</label>

  <label><input type="checkbox" id="continuousWaveform" checked="checked" /> Continuous waveform</label>

  <p id="progress">00:00</p>

  <div id="mic" style="border: 1px solid #ddd; border-radius: 4px; margin-top: 1rem"></div>

  <div id="recordings" style="margin: 1rem 0"></div>

  <style>
    button {
      min-width: 5rem;
      margin: 1rem 1rem 1rem 0;
    }
  </style>
</html>
*/


================================================
FILE: examples/regions.js
================================================
// Regions plugin

import WaveSurfer from 'wavesurfer.js'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'

// Initialize the Regions plugin
const regions = RegionsPlugin.create()

// Create a WaveSurfer instance
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  dragToSeek: false,
  url: '/examples/audio/audio.wav',
  plugins: [regions],
})

// Give regions a random color when they are created
const random = (min, max) => Math.random() * (max - min) + min
const randomColor = () => `rgba(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)}, 0.5)`

// Create some regions at specific time ranges
ws.on('decode', () => {
  // Regions
  regions.addRegion({
    start: 0,
    end: 8,
    content: 'Resize me',
    color: randomColor(),
    drag: false,
    resize: true,
  })
  regions.addRegion({
    start: 9,
    end: 10,
    content: 'Cramped region',
    color: randomColor(),
    minLength: 1,
    maxLength: 10,
  })
  regions.addRegion({
    start: 12,
    end: 17,
    content: 'Drag me',
    color: randomColor(),
    resize: false,
  })

  // Markers (zero-length regions)
  regions.addRegion({
    start: 19,
    content: 'Marker',
    color: randomColor(),
  })
  regions.addRegion({
    start: 20,
    content: 'Second marker',
    color: randomColor(),
  })
})

regions.on('region-updated', (region) => {
  console.log('Updated region', region)
})

// Loop a region on click
let loop = true
// Toggle looping with a checkbox
document.querySelector('#loop').onclick = (e) => {
  loop = e.target.checked
}

// Drag Selection: Create new regions by moving the cursor while holding left-click on the waveform
let dragSelection = undefined

const toggleDragSelection = () => {
  if (!dragSelection) {
    dragSelection = regions.enableDragSelection({
      color: 'rgba(255, 0, 0, 0.1)',
    })
  } else {
    dragSelection()
    dragSelection = undefined
  }
}

// Toggle drag selection with a checkbox
document.querySelector('#dragSelectToggle').addEventListener('change', () => {
  toggleDragSelection()
})

// Drag To Seek
let dragToSeek = false
const toggleDragToSeek = () => {
  console.log(dragToSeek)
  dragToSeek = !dragToSeek
  ws.setOptions({ dragToSeek: dragToSeek })
}

// Toggle drag selection with a checkbox
document.querySelector('#dragToSeekToggle').addEventListener('change', () => {
  toggleDragToSeek()
})

{
  let activeRegion = null
  regions.on('region-in', (region) => {
    console.log('region-in', region)
    activeRegion = region
  })
  regions.on('region-out', (region) => {
    console.log('region-out', region)
    if (activeRegion === region) {
      if (loop) {
        region.play()
      } else {
        activeRegion = null
      }
    }
  })
  regions.on('region-clicked', (region, e) => {
    e.stopPropagation() // prevent triggering a click on the waveform
    activeRegion = region
    region.play(true)
    region.setOptions({ color: randomColor() })
  })
  // Reset the active region when the user clicks anywhere in the waveform
  ws.on('interaction', () => {
    activeRegion = null
  })
}

// Update the zoom level on slider change
ws.once('decode', () => {
  document.querySelector('input[type="range"]').oninput = (e) => {
    const minPxPerSec = Number(e.target.value)
    ws.zoom(minPxPerSec)
  }
})

/*
  <html>
    <div id="waveform"></div>

    <p>
      <label>
        <input id="loop" type="checkbox" checked="${loop}" />
        Loop regions
      </label>

      <label>
        <input id="dragSelectToggle" type="checkbox" style="margin-left: 1em" />
        Enable drag select
      </label>

      <label>
        <input id="dragToSeekToggle" type="checkbox" style="margin-left: 1em" />
        Enable drag to seek
      </label>

      <label style="margin-left: 2em">
        Zoom: <input type="range" min="10" max="1000" value="10" />
      </label>
    </p>

    <p>
      <a href="https://wavesurfer.xyz/docs/classes/plugins_regions.default">Regions plugin docs</a>
    </p>
  </html>
*/


================================================
FILE: examples/silence.js
================================================
// Silence detection example

import WaveSurfer from 'wavesurfer.js'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/nasa.mp4',
  minPxPerSec: 50,
  interact: false,
})

// Initialize the Regions plugin
const wsRegions = ws.registerPlugin(RegionsPlugin.create())

// Find regions separated by silence
const extractRegions = (audioData, duration) => {
  const minValue = 0.01
  const minSilenceDuration = 0.1
  const mergeDuration = 0.2
  const scale = duration / audioData.length
  const silentRegions = []

  // Find all silent regions longer than minSilenceDuration
  let start = 0
  let end = 0
  let isSilent = false
  for (let i = 0; i < audioData.length; i++) {
    if (audioData[i] < minValue) {
      if (!isSilent) {
        start = i
        isSilent = true
      }
    } else if (isSilent) {
      end = i
      isSilent = false
      if (scale * (end - start) > minSilenceDuration) {
        silentRegions.push({
          start: scale * start,
          end: scale * end,
        })
      }
    }
  }

  // Merge silent regions that are close together
  const mergedRegions = []
  let lastRegion = null
  for (let i = 0; i < silentRegions.length; i++) {
    if (lastRegion && silentRegions[i].start - lastRegion.end < mergeDuration) {
      lastRegion.end = silentRegions[i].end
    } else {
      lastRegion = silentRegions[i]
      mergedRegions.push(lastRegion)
    }
  }

  // Find regions that are not silent
  const regions = []
  let lastEnd = 0
  for (let i = 0; i < mergedRegions.length; i++) {
    regions.push({
      start: lastEnd,
      end: mergedRegions[i].start,
    })
    lastEnd = mergedRegions[i].end
  }

  return regions
}

// Create regions for each non-silent part of the audio
ws.on('decode', (duration) => {
  const decodedData = ws.getDecodedData()
  if (decodedData) {
    const regions = extractRegions(decodedData.getChannelData(0), duration)

    // Add regions to the waveform
    regions.forEach((region, index) => {
      wsRegions.addRegion({
        start: region.start,
        end: region.end,
        content: index.toString(),
        drag: false,
        resize: false,
      })
    })
  }
})

// Play a region on click
let activeRegion = null
wsRegions.on('region-clicked', (region, e) => {
  e.stopPropagation()
  region.play()
  activeRegion = region
})
ws.on('timeupdate', (currentTime) => {
  // When the end of the region is reached
  if (activeRegion && currentTime >= activeRegion.end) {
    // Stop playing
    ws.pause()
    activeRegion = null
  }
})


================================================
FILE: examples/soundcloud.js
================================================
// Soundcloud-style player

import WaveSurfer from 'wavesurfer.js'

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

// Define the waveform gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height * 1.35)
gradient.addColorStop(0, '#656666') // Top color
gradient.addColorStop((canvas.height * 0.7) / canvas.height, '#656666') // Top color
gradient.addColorStop((canvas.height * 0.7 + 1) / canvas.height, '#ffffff') // White line
gradient.addColorStop((canvas.height * 0.7 + 2) / canvas.height, '#ffffff') // White line
gradient.addColorStop((canvas.height * 0.7 + 3) / canvas.height, '#B1B1B1') // Bottom color
gradient.addColorStop(1, '#B1B1B1') // Bottom color

// Define the progress gradient
const progressGradient = ctx.createLinearGradient(0, 0, 0, canvas.height * 1.35)
progressGradient.addColorStop(0, '#EE772F') // Top color
progressGradient.addColorStop((canvas.height * 0.7) / canvas.height, '#EB4926') // Top color
progressGradient.addColorStop((canvas.height * 0.7 + 1) / canvas.height, '#ffffff') // White line
progressGradient.addColorStop((canvas.height * 0.7 + 2) / canvas.height, '#ffffff') // White line
progressGradient.addColorStop((canvas.height * 0.7 + 3) / canvas.height, '#F6B094') // Bottom color
progressGradient.addColorStop(1, '#F6B094') // Bottom color

// Create the waveform
const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: gradient,
  progressColor: progressGradient,
  barWidth: 2,
  url: '/examples/audio/audio.wav',
})

// Play/pause on click
wavesurfer.on('interaction', () => {
  wavesurfer.playPause()
})

// Hover effect
{
  const hover = document.querySelector('#hover')
  const waveform = document.querySelector('#waveform')
  waveform.addEventListener('pointermove', (e) => (hover.style.width = `${e.offsetX}px`))
}

// Current time & duration
{
  const formatTime = (seconds) => {
    const minutes = Math.floor(seconds / 60)
    const secondsRemainder = Math.round(seconds) % 60
    const paddedSeconds = `0${secondsRemainder}`.slice(-2)
    return `${minutes}:${paddedSeconds}`
  }

  const timeEl = document.querySelector('#time')
  const durationEl = document.querySelector('#duration')
  wavesurfer.on('decode', (duration) => (durationEl.textContent = formatTime(duration)))
  wavesurfer.on('timeupdate', (currentTime) => (timeEl.textContent = formatTime(currentTime)))
}

/*
<html>
  <style>
    #waveform {
      cursor: pointer;
      position: relative;
    }
    #hover {
      position: absolute;
      left: 0;
      top: 0;
      z-index: 10;
      pointer-events: none;
      height: 100%;
      width: 0;
      mix-blend-mode: overlay;
      background: rgba(255, 255, 255, 0.5);
      opacity: 0;
      transition: opacity 0.2s ease;
    }
    #waveform:hover #hover {
      opacity: 1;
    }
    #time,
    #duration {
      position: absolute;
      z-index: 11;
      top: 50%;
      margin-top: -1px;
      transform: translateY(-50%);
      font-size: 11px;
      background: rgba(0, 0, 0, 0.75);
      padding: 2px;
      color: #ddd;
    }
    #time {
      left: 0;
    }
    #duration {
      right: 0;
    }
  </style>
  <div id="waveform">
    <div id="time">0:00</div>
    <div id="duration">0:00</div>
    <div id="hover"></div>
  </div>
</html>
*/


================================================
FILE: examples/spectrogram-windowed.js
================================================
// Windowed Spectrogram plugin - Optimized for very long audio files

import WaveSurfer from 'wavesurfer.js'
import WindowedSpectrogram from 'wavesurfer.js/dist/plugins/spectrogram-windowed.esm.js'
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js'
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/librivox.mp3',
  sampleRate: 44100,
  minPxPerSec: 100,
})

// Initialize the Windowed Spectrogram plugin
ws.registerPlugin(
  WindowedSpectrogram.create({
    labels: true,
    splitChannels: true,
    scale: 'mel', // or 'linear', 'logarithmic', 'bark', 'erb'
    frequencyMax: 18000,
    frequencyMin: 0,
    fftSamples: 1024, // Use a reasonable FFT size (powers of 2: 256, 512, 1024, 2048)
    labelsBackground: 'rgba(0, 0, 0, 0.1)',
    colorMap: 'roseus', // Color scheme optimized for long audio viewing
    useWebWorker: true,
    progressiveLoading: true,
  }),
)

// Initialize the TimeLabels plugin
ws.registerPlugin(
  TimelinePlugin.create({
    labels: true,
    labelsBackground: 'rgba(0, 0, 0, 0.1)',
  }),
)

// Initialize the Zoom plugin for interactive zooming
ws.registerPlugin(
  ZoomPlugin.create({
    scale: 0.5, // 50% zoom per wheel step
    maxZoom: 1000, // Allow zooming up to 1000 px/sec
  }),
)

// Show the current zoom level
ws.on('zoom', (minPxPerSec) => {
  const zoomDisplay = document.querySelector('#zoom-level')
  if (zoomDisplay) {
    zoomDisplay.textContent = `${Math.round(minPxPerSec)} px/s`
  }
})

// Play on click
ws.once('interaction', () => {
  ws.play()
})

/*
<html>
  <div style="margin-bottom: 10px;">
    Zoom level: <span id="zoom-level">50 px/s</span>
  </div>
  <div id="waveform"></div>
  <p>
    📖 <a href="https://wavesurfer.xyz/docs/modules/plugins_spectrogram">Windowed Spectrogram plugin docs</a>
  </p>
  <p>
    ⚡ This plugin is optimized for very long audio files by using a sliding window approach
    that keeps memory usage constant regardless of audio length.
  </p>
  <p>
    🔍 Use mouse wheel to zoom in/out. The spectrogram will dynamically load segments as you navigate.
    Notice how segments are loaded on-demand as you zoom and scroll!
  </p>
</html>
*/


================================================
FILE: examples/spectrogram.js
================================================
// Spectrogram plugin example

import WaveSurfer from 'wavesurfer.js'
import Spectrogram from 'wavesurfer.js/dist/plugins/spectrogram.esm.js'

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  sampleRate: 44100,
})

// Initialize the Spectrogram plugin with detailed configuration
ws.registerPlugin(
  Spectrogram.create({
    // Display frequency labels on the left side
    labels: true,

    // Height of the spectrogram in pixels
    height: 200,

    // Render separate spectrograms for each audio channel
    // Set to false to combine all channels into one spectrogram
    splitChannels: true,

    // Frequency scale type:
    // - 'linear': Standard linear frequency scale (0-20kHz)
    // - 'logarithmic': Logarithmic scale, better for low frequencies
    // - 'mel': Mel scale based on human hearing perception (default)
    // - 'bark': Bark scale for psychoacoustic analysis
    // - 'erb': ERB scale for auditory filter modeling
    scale: 'mel',

    // Frequency range to display (in Hz)
    frequencyMax: 8000, // Maximum frequency to show
    frequencyMin: 0, // Minimum frequency to show

    // FFT parameters
    fftSamples: 1024, // Number of samples for FFT (must be power of 2)
    // Higher values = better frequency resolution, slower rendering

    // Visual styling
    labelsBackground: 'rgba(0, 0, 0, 0.1)', // Background for frequency labels

    // Performance optimization
    useWebWorker: true, // Use web worker for FFT calculations (improves performance)

    // Additional options you can configure:
    //
    // Window function for FFT (affects frequency resolution vs time resolution):
    // windowFunc: 'hann' | 'hamming' | 'blackman' | 'bartlett' | 'cosine' | 'gauss' | 'lanczoz' | 'rectangular' | 'triangular'
    //
    // Color mapping for frequency intensity:
    // colorMap: 'gray' | 'igray' | 'roseus' | custom array
    //
    // Gain and range for color scaling:
    // gainDB: 20,        // Brightness adjustment (default: 20dB)
    // rangeDB: 80,       // Dynamic range (default: 80dB)
    //
    // Overlap between FFT windows:
    // noverlap: null,    // Auto-calculated by default, or set manually
    //
    // Maximum canvas width for performance:
    // maxCanvasWidth: 30000,  // Split large spectrograms into multiple canvases
  }),
)

// Play audio when user clicks on the waveform
ws.once('interaction', () => {
  ws.play()
})

// Event listeners for spectrogram interactions
ws.on('spectrogram-ready', () => {
  console.log('Spectrogram has finished rendering')
})

ws.on('spectrogram-click', (relativeX) => {
  console.log('Clicked on spectrogram at position:', relativeX)
  // You can use relativeX to seek to that position in the audio
  ws.setTime(relativeX * ws.getDuration())
})

/*
<html>
  <div id="waveform"></div>
  
  <!-- Configuration Options -->
  <div style="margin-top: 20px; padding: 15px; border-radius: 8px;">
    <div style=" border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin-bottom: 15px;">
      <strong>⚠️ Important Note:</strong> For audio files that require scrolling (longer than the container width), 
      you <strong>MUST</strong> set a <code>minPxPerSec</code> value in the WaveSurfer configuration to ensure 
      proper spectrogram rendering. Without this, the spectrogram may not display correctly.
    </div>
    
    <h3>Spectrogram Settings</h3>
    
    <h4>Visual Options</h4>
    <ul>
      <li><code>labels: true/false</code> - Show frequency labels on the left</li>
      <li><code>height: 200</code> - Spectrogram height in pixels</li>
      <li><code>splitChannels: true/false</code> - Separate spectrograms for each audio channel</li>
      <li><code>labelsBackground: 'rgba(0,0,0,0.1)'</code> - Background color for labels</li>
      <li><code>labelsColor: '#fff'</code> - Text color for frequency labels</li>
    </ul>
    
    <h4>Frequency Settings</h4>
    <ul>
      <li><code>scale: 'mel'|'linear'|'logarithmic'|'bark'|'erb'</code> - Frequency scale type</li>
      <li><code>frequencyMax: 8000</code> - Maximum frequency to display (Hz)</li>
      <li><code>frequencyMin: 0</code> - Minimum frequency to display (Hz)</li>
    </ul>
    
    <h4>Performance Settings</h4>
    <ul>
      <li><code>fftSamples: 1024</code> - FFT resolution (512, 1024, 2048, 4096)</li>
      <li><code>useWebWorker: true</code> - Use web worker for faster processing</li>
      <li><code>maxCanvasWidth: 30000</code> - Split large spectrograms into multiple canvases</li>
      <li><code>noverlap: null</code> - Overlap between FFT windows (auto-calculated)</li>
    </ul>
    
    <h4>Color & Styling</h4>
    <ul>
      <li><code>colorMap: 'gray'|'igray'|'roseus'</code> - Color scheme for frequency intensity</li>
      <li><code>gainDB: 20</code> - Brightness adjustment (-20 to +40)</li>
      <li><code>rangeDB: 80</code> - Dynamic range (20 to 120)</li>
      <li><code>windowFunc: 'hann'</code> - FFT window function (hann, hamming, blackman, etc.)</li>
    </ul>
    
    <p style="margin-top: 15px; font-size: 14px;">
      📖 <a href="https://wavesurfer.xyz/docs/modules/plugins_spectrogram">Full Documentation</a>
    </p>
  </div>
</html>
*/


================================================
FILE: examples/speed.js
================================================
// Set the playback speed

/*
<html>
  <div style="display: flex; margin: 1rem 0; gap: 1rem;">
    <button>
      Play/pause
    </button>

    <label>
      Playback rate: <span id="rate">2.00</span>x
    </label>

    <label>
      0.25x <input type="range" min="0" max="4" step="1" value="2" /> 4x
    </label>

    <label>
      <input type="checkbox" checked />
      Preserve pitch
    </label>
  </div>
</html>
*/

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/librivox.mp3',
  audioRate: 2, // set the initial playback rate
})

let preservePitch = true
const speeds = [0.25, 0.5, 1, 2, 4]

// Toggle pitch preservation
document.querySelector('input[type="checkbox"]').addEventListener('change', (e) => {
  preservePitch = e.target.checked
  wavesurfer.setPlaybackRate(wavesurfer.getPlaybackRate(), preservePitch)
})

// Set the playback rate
document.querySelector('input[type="range"]').addEventListener('input', (e) => {
  const speed = speeds[e.target.valueAsNumber]
  document.querySelector('#rate').textContent = speed.toFixed(2)
  wavesurfer.setPlaybackRate(speed, preservePitch)
  wavesurfer.play()
})

// Play/pause
document.querySelector('button').addEventListener('click', () => {
  wavesurfer.playPause()
})


================================================
FILE: examples/split-channels.js
================================================
// Split channels

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  url: '/examples/audio/stereo.mp3',
  splitChannels: [
    {
      waveColor: 'rgb(200, 0, 200)',
      progressColor: 'rgb(100, 0, 100)',
    },
    {
      waveColor: 'rgb(0, 200, 200)',
      progressColor: 'rgb(0, 100, 100)',
    },
  ],
})

wavesurfer.on('interaction', () => {
  wavesurfer.play()
})


================================================
FILE: examples/styling.js
================================================
// Custom styling via CSS

/*
  <html>
    <style>
      #waveform ::part(wrapper) {
        --box-size: 10px;
        background-image: 
          linear-gradient(transparent calc(var(--box-size) - 1px), blue var(--box-size), transparent var(--box-size)), 
          linear-gradient(90deg, transparent calc(var(--box-size) - 1px), blue var(--box-size), transparent var(--box-size));
        background-size: 100% var(--box-size), var(--box-size) 100%;
      }

      #waveform ::part(cursor) {
        height: 100px;
        top: 28px;
        border-radius: 4px;
        border: 1px solid #fff;
      }

      #waveform ::part(cursor):after {
        content: '🏄';
        font-size: 1.5em;
        position: absolute;
        left: 0;
        top: -28px;
        transform: translateX(-50%);
      }

      #waveform ::part(region) {
        background-color: rgba(0, 0, 100, 0.25) !important;
      }

      #waveform ::part(region-green) {
        background-color: rgba(0, 100, 0, 0.25) !important;
        font-size: 12px;
        text-shadow: 0 0 2px #fff;
      }

      #waveform ::part(marker) {
        background-color: rgba(0, 0, 100, 0.25) !important;
        border: 1px solid #fff;
        padding: 1px;
        text-indent: 10px;
        font-family: fantasy;
        text-decoration: underline;
      }

      #waveform ::part(region-handle-right) {
        border-right-width: 4px !important;
        border-right-color: #fff000 !important;
      }
    </style>

    <div id="waveform"></div>
  </html>
*/

import WaveSurfer from 'wavesurfer.js'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'

// Create a Regions plugin instance
const wsRegions = RegionsPlugin.create()

// Create an instance of WaveSurfer
const ws = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'hotpink',
  progressColor: 'paleturquoise',
  cursorColor: '#57BAB6',
  cursorWidth: 4,
  minPxPerSec: 100,
  url: '/examples/audio/audio.wav',
  plugins: [wsRegions],
})

// Create some regions at specific time ranges
ws.on('decode', () => {
  wsRegions.addRegion({
    start: 4,
    end: 7,
    content: 'Blue',
  })

  wsRegions.addRegion({
    id: 'region-green',
    start: 10,
    end: 12,
    content: 'Green',
  })

  wsRegions.addRegion({
    start: 19,
    content: 'Marker',
  })
})


================================================
FILE: examples/timeline-custom.js
================================================
// Customized Timeline plugin

import WaveSurfer from 'wavesurfer.js'
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'

// Create a timeline plugin instance with custom options
const topTimeline = TimelinePlugin.create({
  height: 20,
  insertPosition: 'beforebegin',
  timeInterval: 0.2,
  primaryLabelInterval: 5,
  secondaryLabelInterval: 1,
  style: {
    fontSize: '20px',
    color: '#2D5B88',
  },
})

// Create a second timeline
const bottomTimeline = TimelinePlugin.create({
  height: 10,
  timeInterval: 0.1,
  primaryLabelInterval: 1,
  style: {
    fontSize: '10px',
    color: '#6A3274',
  },
})

// Create an instance of WaveSurfer
const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  minPxPerSec: 100,
  plugins: [topTimeline, bottomTimeline],
})

// Play on click
wavesurfer.once('interaction', () => {
  wavesurfer.play()
})

/*
<html>
  <div id="waveform"></div>
  <p>
    📖 <a href="https://wavesurfer.xyz/docs/modules/plugins_timeline">Timeline plugin docs</a>
  </p>
</html>
*/


================================================
FILE: examples/timeline.js
================================================
// Timeline plugin

import WaveSurfer from 'wavesurfer.js'
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'

// Create an instance of WaveSurfer
const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  minPxPerSec: 100,
  plugins: [TimelinePlugin.create()],
})

// Play on click
wavesurfer.on('interaction', () => {
  wavesurfer.play()
})

// Rewind to the beginning on finished playing
wavesurfer.on('finish', () => {
  wavesurfer.setTime(0)
})

/*
<html>
  <label>
    Zoom: <input type="range" min="10" max="1000" value="100" />
  </label>

  <div id="waveform"></div>
  <p>
    📖 <a href="https://wavesurfer.xyz/docs/classes/plugins_timeline.TimelinePlugin">Timeline plugin docs</a>
  </p>
</html>
*/

// Update the zoom level on slider change
wavesurfer.once('decode', () => {
  const slider = document.querySelector('input[type="range"]')

  slider.addEventListener('input', (e) => {
    const minPxPerSec = e.target.valueAsNumber
    wavesurfer.zoom(minPxPerSec)
  })
})


================================================
FILE: examples/video.js
================================================
// Waveform for a video

// Create a video element
/*
<html>
  <video
    src="/examples/audio/modular.mp4"
    controls
    playsinline
    style="width: 100%; max-width: 600px; margin: 0 auto; display: block;"
  />
</html>
*/

import WaveSurfer from 'wavesurfer.js'

// Initialize wavesurfer.js
const ws = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  // Pass the video element in the `media` param
  media: document.querySelector('video'),
})


================================================
FILE: examples/vowels.js
================================================
// American English vowels

import WaveSurfer from 'wavesurfer.js'
import Spectrogram from 'wavesurfer.js/dist/plugins/spectrogram.esm.js'

// Sounds generated with `say -v 'Reed (English (US))' word`
const vowels = ['i', 'ɪ', 'ɛ', 'æ', 'ɑ', 'ɔ', 'o', 'ʊ', 'u', 'ʌ', 'ə', 'ɝ']
const files = ['ee', 'ih', 'hen', 'hat', 'ah', 'hot', 'oh', 'hook', 'oo', 'uh', 'ahoy', 'er']

const grid = document.querySelector('.grid')
const containers = vowels.map((vowel) => {
  const vowelDiv = document.createElement('div')
  vowelDiv.textContent = `[ ${vowel} ]`
  return grid.appendChild(vowelDiv)
})

containers.forEach((vowelDiv, idx) => {
  const wavesurfer = WaveSurfer.create({
    container: vowelDiv,
    height: 50,
    hideScrollbar: true,
    waveColor: 'rgb(200, 0, 200)',
    progressColor: 'rgb(100, 0, 100)',
    url: `/examples/audio/${files[idx]}.mp4`,
    sampleRate: 14600,
    interact: false,
    plugins: [
      Spectrogram.create({
        labels: true,
        labelsColor: 'currentColor',
        labelsBackground: 'transparent',
        height: 150,
      }),
    ],
  })

  wavesurfer.on('ready', () => {
    vowelDiv.onclick = () => {
      wavesurfer.playPause()
    }
  })
})

/*
<html>
  <div class="grid"></div>

  <style>
  .grid {
    display: flex;
    flex-flow: row wrap;
    gap: 2px;
  }
  .grid > div {
    min-width: 120px;
    padding: 0.5rem;
    text-align: center;
    border: 1px solid #333;
    border-radius: 4px;
    cursor: pointer;
  }
  ::part(spec-labels) {
    position: absolute;
    right: 0;
  }
  </style>
</html>
*/


================================================
FILE: examples/webaudio-shim.js
================================================
import WaveSurfer from 'wavesurfer.js'
import WebAudioPlayer from 'wavesurfer.js/dist/webaudio.js'

const webAudioPlayer = new WebAudioPlayer()
webAudioPlayer.src = '/examples/audio/audio.wav'

webAudioPlayer.addEventListener('loadedmetadata', () => {
  const wavesurfer = WaveSurfer.create({
    container: document.body,
    media: webAudioPlayer,
    peaks: webAudioPlayer.getChannelData(),
    duration: webAudioPlayer.duration,
  })

  wavesurfer.on('click', () => {
    wavesurfer.play()
  })
})


================================================
FILE: examples/webaudio.js
================================================
// Web Audio example

import WaveSurfer from 'wavesurfer.js'

// Define the equalizer bands
const eqBands = [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]

// Create a WaveSurfer instance and pass the media element
const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  mediaControls: true,
})

wavesurfer.on('click', () => wavesurfer.playPause())

wavesurfer.once('play', () => {
  // Create Web Audio context
  const audioContext = new AudioContext()

  // Create a biquad filter for each band
  const filters = eqBands.map((band) => {
    const filter = audioContext.createBiquadFilter()
    filter.type = band <= 32 ? 'lowshelf' : band >= 16000 ? 'highshelf' : 'peaking'
    filter.gain.value = Math.random() * 40 - 20
    filter.Q.value = 1 // resonance
    filter.frequency.value = band // the cut-off frequency
    return filter
  })

  const audio = wavesurfer.getMediaElement()
  const mediaNode = audioContext.createMediaElementSource(audio)

  // Connect the filters and media node sequentially
  const equalizer = filters.reduce((prev, curr) => {
    prev.connect(curr)
    return curr
  }, mediaNode)

  // Connect the filters to the audio output
  equalizer.connect(audioContext.destination)

  sliders.forEach((slider, i) => {
    const filter = filters[i]
    filter.gain.value = slider.value
    slider.oninput = (e) => (filter.gain.value = e.target.value)
  })
})

// HTML UI
// Create a vertical slider for each band
const container = document.createElement('p')
const sliders = eqBands.map(() => {
  const slider = document.createElement('input')
  slider.type = 'range'
  slider.orient = 'vertical'
  slider.style.appearance = 'slider-vertical'
  slider.style.width = '8%'
  slider.min = -40
  slider.max = 40
  slider.value = Math.random() * 40 - 20
  slider.step = 0.1
  container.appendChild(slider)
  return slider
})
document.body.appendChild(container)


================================================
FILE: examples/zoom-plugin.js
================================================
/**
 * Zoom plugin
 *
 * Zoom in or out on the waveform when scrolling the mouse wheel
 */

import WaveSurfer from 'wavesurfer.js'
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js'

// Create an instance of WaveSurfer
const wavesurfer = WaveSurfer.create({
  container: '#waveform',
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  minPxPerSec: 100,
})

// Initialize the Zoom plugin
wavesurfer.registerPlugin(
  ZoomPlugin.create({
    // the amount of zoom per wheel step, e.g. 0.5 means a 50% magnification per scroll
    scale: 0.5,
    // Optionally, specify the maximum pixels-per-second factor while zooming
    maxZoom: 100,
  }),
)

//  show the current minPxPerSec value
const minPxPerSecSpan = document.querySelector('#minPxPerSec')
wavesurfer.on('zoom', (minPxPerSec) => {
  minPxPerSecSpan.textContent = `${Math.round(minPxPerSec)}`
})

// Create a minPxPerSec display and waveform container
/*
<html>
  <div>
       minPxPerSec: <span id="minPxPerSec">100</span> px/s
  </div>

    <div id="waveform"></div>
 </html>
 *
 */

// A few more controls
/*
<html>
    <button id="play">Play/Pause</button>
    <button id="backward">Backward 5s</button>
    <button id="forward">Forward 5s</button>
  <p>
    📖 Zoom in or out on the waveform when scrolling the mouse wheel
  </p>
</html>
*/

const playButton = document.querySelector('#play')
const forwardButton = document.querySelector('#forward')
const backButton = document.querySelector('#backward')

playButton.onclick = () => {
  wavesurfer.playPause()
}

forwardButton.onclick = () => {
  wavesurfer.skip(5)
}

backButton.onclick = () => {
  wavesurfer.skip(-5)
}


================================================
FILE: examples/zoom.js
================================================
// Zooming the waveform

import WaveSurfer from 'wavesurfer.js'

const wavesurfer = WaveSurfer.create({
  container: document.body,
  waveColor: 'rgb(200, 0, 200)',
  progressColor: 'rgb(100, 0, 100)',
  url: '/examples/audio/audio.wav',
  minPxPerSec: 100,
  dragToSeek: true,
})

// Create a simple slider
/*
<html>
  <label>
    Zoom: <input type="range" min="10" max="1000" value="100" />
  </label>
</html>
*/

// Update the zoom level on slider change
wavesurfer.once('decode', () => {
  const slider = document.querySelector('input[type="range"]')

  slider.addEventListener('input', (e) => {
    const minPxPerSec = e.target.valueAsNumber
    wavesurfer.zoom(minPxPerSec)
  })
})

// A few more controls

/*
<html>
  <label><input type="checkbox" checked value="scrollbar" /> Scroll bar</label>
  <label><input type="checkbox" checked value="fillParent" /> Fill parent</label>
  <label><input type="checkbox" checked value="autoCenter" /> Auto center</label>

  <div style="margin: 1em 0 2em;">
    <button id="play">Play/Pause</button>
    <button id="backward">Backward 5s</button>
    <button id="forward">Forward 5s</button>
  </div>
</html>
*/

const playButton = document.querySelector('#play')
const forwardButton = document.querySelector('#forward')
const backButton = document.querySelector('#backward')

wavesurfer.once('decode', () => {
  document.querySelectorAll('input[type="checkbox"]').forEach((input) => {
    input.onchange = (e) => {
      wavesurfer.setOptions({
        [input.value]: e.target.checked,
      })
    }
  })

  playButton.onclick = () => {
    wavesurfer.playPause()
  }

  forwardButton.onclick = () => {
    wavesurfer.skip(5)
  }

  backButton.onclick = () => {
    wavesurfer.skip(-5)
  }
})


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>wavesurfer.js examples</title>

    <style>
      body {
        box-sizing: border-box;
        display: flex;
        flex-direction: column;
        gap: 1rem;
        padding: 0;
        margin: 0;
        min-height: 100vh;
        font-size: 16px;
        font-family: sans-serif;
      }
      body * {
        box-sizing: border-box;
      }
      header {
        width: 100%;
        text-align: center;
        padding: 1rem 1rem 0;
      }
      header h1 {
        margin: 0;
        font-size: 1.3em;
      }
      aside {
        max-height: 100%;
        overflow-y: auto;
        overflow-x: hidden;
        min-width: 130px;
      }
      aside ul {
        margin: 0;
        padding: 0;
        list-style: none;
      }
      aside li {
        margin-bottom: 0.5rem;
      }
      aside a.active {
        font-weight: bold;
        text-decoration: none;
      }
      main {
        flex: 1;
        display: flex;
        gap: 1rem;
        padding: 0 1rem;
      }
      iframe {
        display: block;
        flex: 1;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      textarea {
        display: block;
        width: 40%;
        font-family: 'Menlo', monospace;
        font-size: 13px;
        padding: 1em;
        border: 1px solid #ccc;
        border-radius: 4px;
      }
      footer {
        padding: 0 1rem 1rem;
        display: flex;
        gap: 1rem;
        justify-content: center;
        align-items: center;
      }

      @media (max-width: 768px) {
        aside {
          padding-bottom: 0;
        }
        aside ul {
          width: 100%;
          display: flex;
          flex-wrap: nowrap;
          overflow-x: auto;
          gap: 1rem;
          padding-bottom: 1rem;
        }
        aside li {
          white-space: nowrap;
        }
        main {
          flex-direction: column;
        }
        iframe {
          width: 100%;
          height: 20vh;
        }
        textarea {
          width: 100%;
          order: 2;
          flex: 1;
        }
      }

      @media (prefers-color-scheme: dark) {
        body {
          background: #222;
          color: #eee;
        }
        body a {
          color: #fff;
        }
        iframe {
          border-color: #444;
          background: #333;
        }
        textarea {
          background: #333;
          color: #eee;
          border-color: #444;
        }
      }
    </style>
  </head>

  <body>
    <header>
      <h1>wavesurfer.js examples</h1>
    </header>

    <main>
      <aside>
        <h3>Basics</h3>
        <ul>
          <li><a href="#basic.js">Basic</a></li>
          <li><a href="#all-options.js">Options</a></li>
          <li><a href="#events.js">Events</a></li>
          <li><a href="#zoom.js">Zoom</a></li>
          <li><a href="#bars.js">Bars</a></li>
          <li><a href="#react.js">React</a></li>
          <li><a href="#predecoded.js">Pre-decoded</a></li>
          <li><a href="#video.js">Video</a></li>
          <li><a href="#speed.js">Speed</a></li>
        </ul>

        <h3>Plugins</h3>
        <ul>
          <li><a href="#regions.js">Regions</a></li>
          <li><a href="#hover.js">Hover</a></li>
          <li><a href="#timeline.js">Timeline</a></li>
          <li><a href="#timeline-custom.js">Timeline x2</a></li>
          <li><a href="#minimap.js">Minimap</a></li>
          <li><a href="#envelope.js">Envelope</a></li>
          <li><a href="#spectrogram.js">Spectrogram</a></li>
          <li><a href="#spectrogram-windowed.js">Spectrogram Windowed</a></li>
          <li><a href="#record.js">Record</a></li>
          <li><a href="#zoom-plugin.js">Zoom</a></li>
        </ul>

        <h3>Advanced</h3>
        <ul>
          <li><a href="#styling.js">Styling</a></li>
          <li><a href="#gradient.js">Gradient</a></li>
          <li><a href="#soundcloud.js">Soundcloud</a></li>
          <li><a href="#webaudio.js">Web Audio</a></li>
          <li><a href="#silence.js">Silence</a></li>
          <li><a href="#pitch.js">Pitch</a></li>
          <li><a href="#split-channels.js">Split channels</a></li>
          <li><a href="#custom-render.js">Custom render</a></li>
          <li><a href="#multitrack.js">Multi-track</a></li>
          <li><a href="#vowels.js">Vowels</a></li>
          <li><a href="#fm-synth.js">FM synth</a></li>
        </ul>
      </aside>

      <textarea spellcheck="false"></textarea>
      <iframe id="preview" sandbox="allow-scripts allow-same-origin" title="wavesurfer.js example preview"></iframe>
    </main>

    <footer>
      <a href="https://github.com/katspaugh/wavesurfer.js">GitHub</a>
    </footer>

    <script type="module" src="/examples/_preview.js"></script>
  </body>
</html>


================================================
FILE: jest.config.js
================================================
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/src'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  collectCoverage: true,
  collectCoverageFrom: ['src/**/*.ts'],
  globals: {
    'ts-jest': {
      useESM: true,
      tsconfig: 'tsconfig.test.json',
    },
  },
}


================================================
FILE: package.json
================================================
{
  "name": "wavesurfer.js",
  "version": "7.12.4",
  "license": "BSD-3-Clause",
  "author": "katspaugh",
  "description": "Audio waveform player",
  "homepage": "https://wavesurfer.xyz",
  "keywords": [
    "waveform",
    "spectrogram",
    "audio",
    "player",
    "music",
    "linguistics"
  ],
  "repository": {
    "type": "git",
    "url": "git@github.com:katspaugh/wavesurfer.js.git"
  },
  "type": "module",
  "files": [
    "dist"
  ],
  "main": "./dist/wavesurfer.js",
  "unpkg": "./dist/wavesurfer.min.js",
  "module": "./dist/wavesurfer.js",
  "browser": "./dist/wavesurfer.js",
  "types": "./dist/wavesurfer.d.ts",
  "exports": {
    ".": {
      "import": "./dist/wavesurfer.esm.js",
      "types": "./dist/wavesurfer.d.ts",
      "require": "./dist/wavesurfer.cjs"
    },
    "./dist/plugins/*.js": {
      "import": "./dist/plugins/*.esm.js",
      "types": "./dist/plugins/*.d.ts",
      "require": "./dist/plugins/*.cjs"
    },
    "./plugins/*": {
      "import": "./dist/plugins/*.esm.js",
      "types": "./dist/plugins/*.d.ts",
      "require": "./dist/plugins/*.cjs"
    },
    "./dist/*": {
      "import": "./dist/*",
      "types": "./dist/*.d.ts",
      "require": "./dist/*.cjs"
    },
    "./dist/plugins/*.esm.js": {
      "import": "./dist/plugins/*.esm.js",
      "types": "./dist/plugins/*.d.ts",
      "require": "./dist/plugins/*.cjs"
    }
  },
  "scripts": {
    "clean": "node ./scripts/clean.cjs",
    "build:dev": "tsc -w --target ESNext",
    "build": "npm run clean && tsc && rollup -c",
    "prepublishOnly": "npm run build",
    "lint": "eslint \"src/**/*.ts\" --fix",
    "lint:report": "eslint \"src/**/*.ts\" --output-file eslint_report.json --format json",
    "prettier": "prettier -w '**/*.{js,ts,css}' --ignore-path .gitignore",
    "make-plugin": "./scripts/plugin.sh",
    "cypress": "cypress open --e2e",
    "cypress:canary": "cypress open --e2e -b chrome:canary",
    "test": "cypress run --browser chrome",
    "test:unit": "jest --coverage",
    "serve": "npx live-server --port=9090 --no-browser --ignore='.*,src,cypress,scripts'",
    "start": "npm run build:dev & npm run serve",
    "prepare": "npm run build"
  },
  "packageManager": "yarn@1.22.22",
  "devDependencies": {
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.1",
    "@types/jest": "^29.5.2",
    "@typescript-eslint/eslint-plugin": "^8.43.0",
    "@typescript-eslint/parser": "^8.43.0",
    "cypress": "^13.16.1",
    "cypress-image-snapshot": "^4.0.1",
    "eslint": "^9.35.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-prettier": "^5.2.1",
    "glob": "^11.0.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "prettier": "^3.4.2",
    "rollup": "^4.50.1",
    "rollup-plugin-dts": "^6.1.0",
    "rollup-plugin-web-worker-loader": "^1.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.9.2"
  }
}


================================================
FILE: rollup.config.js
================================================
import { glob } from 'glob'
import typescript from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'
import dts from 'rollup-plugin-dts'
import webWorkerLoader from 'rollup-plugin-web-worker-loader'

const plugins = [
  webWorkerLoader(),
  typescript({ declaration: false, declarationDir: null }),
  terser({ format: { comments: false } }),
]

export default [
  // ES module
  {
    input: 'src/wavesurfer.ts',
    output: {
      file: 'dist/wavesurfer.esm.js',
      format: 'esm',
    },
    plugins,
  },
  // CommonJS module (Node.js)
  {
    input: 'src/wavesurfer.ts',
    output: {
      file: 'dist/wavesurfer.cjs',
      format: 'cjs',
      exports: 'default',
    },
    plugins,
  },
  // UMD (browser script tag)
  {
    input: 'src/wavesurfer.ts',
    output: {
      name: 'WaveSurfer',
      file: 'dist/wavesurfer.min.js',
      format: 'umd',
      exports: 'default',
    },
    plugins,
  },

  // Compiled type definitions
  {
    input: './dist/wavesurfer.d.ts',
    output: [{ file: 'dist/types.d.ts', format: 'es' }],
    plugins: [dts()],
  },

  // Wavesurfer plugins (exclude worker files)
  ...glob
    .sync('src/plugins/*.ts')
    .filter((plugin) => !plugin.includes('worker'))
    .map((plugin) => [
      // ES module
      {
        input: plugin,
        output: {
          file: plugin.replace('src/', 'dist/').replace('.ts', '.js'),
          format: 'esm',
        },
        plugins,
      },
      // ES module again but with an .esm.js extension
      {
        input: plugin,
        output: {
          file: plugin.replace('src/', 'dist/').replace('.ts', '.esm.js'),
          format: 'esm',
        },
        plugins,
      },
      // CommonJS module (Node.js)
      {
        input: plugin,
        output: {
          name: plugin.replace('src/plugins/', '').replace('.ts', ''),
          file: plugin.replace('src/', 'dist/').replace('.ts', '.cjs'),
          format: 'cjs',
          exports: 'default',
        },
        plugins,
      },
      // UMD (browser script tag)
      {
        input: plugin,
        output: {
          name: plugin
            .replace('src/plugins/', '')
            .replace('.ts', '')
            .replace(/^./, (c) => `WaveSurfer.${c.toUpperCase()}`),
          file: plugin.replace('src/', 'dist/').replace('.ts', '.min.js'),
          format: 'umd',
          extend: true,
          globals: {
            WaveSurfer: 'WaveSurfer',
          },
          exports: 'default',
        },
        external: ['WaveSurfer'],
        plugins,
      },
    ])
    .flat(),
]


================================================
FILE: scripts/clean.cjs
================================================
const path = require('path')
const fs = require('fs')

const run = () => {
  const distPath = path.join(__dirname, '../dist')
  fs.rmSync(distPath, { recursive: true, force: true })
}

run()

================================================
FILE: scripts/plugin.sh
================================================
#!/bin/bash

# Plugin name from argument
PLUGIN_NAME=$1

# Prompt for plugin name if not provided
if [ -z "$PLUGIN_NAME" ]
then
  echo "Enter plugin name: "
  read PLUGIN_NAME
fi

FILE_NAME=$(echo "$PLUGIN_NAME" | sed -e 's/\(.*\)/\L\1/')

cat ./scripts/plugin-template.ts.template | sed "s/Template/$PLUGIN_NAME/g" > "./src/plugins/${FILE_NAME}.ts"


================================================
FILE: scripts/plugin.ts.template
================================================
/**
 * The Template plugin
 */

import BasePlugin, { type BasePluginEvents } from '../base-plugin.js'

export type TemplatePluginOptions = {
}

const defaultOptions = {
}

export type TemplatePluginEvents = BasePluginEvents & {
}

export class TemplatePlugin extends BasePlugin<TemplatePluginEvents, TemplatePluginOptions> {
  protected options: TemplatePluginOptions & typeof defaultOptions

  constructor(options?: TemplatePluginOptions) {
    super(options || {})

    this.options = Object.assign({}, defaultOptions, options)
  }

  public static create(options?: TemplatePluginOptions) {
    return new TemplatePlugin(options)
  }

  /** Called by wavesurfer, don't call manually */
  onInit() {
    if (!this.wavesurfer) {
      throw Error('WaveSurfer is not initialized')
    }
  }

  /** Unmount */
  public destroy() {
    super.destroy()
  }
}

export default TemplatePlugin


================================================
FILE: src/__tests__/base-plugin.test.ts
================================================
import { BasePlugin } from '../base-plugin.js'

class TestPlugin extends BasePlugin<{ destroy: [] }, {}> {
  initCalled = false
  protected onInit() {
    this.initCalled = true
  }
}

describe('BasePlugin', () => {
  test('_init calls onInit and sets wavesurfer', () => {
    const plugin = new TestPlugin({})
    const ws = {} as any
    plugin._init(ws)
    expect((plugin as any).wavesurfer).toBe(ws)
    expect(plugin.initCalled).toBe(true)
  })

  test('destroy emits destroy and unsubscribes', () => {
    const plugin = new TestPlugin({})
    const unsub = jest.fn()
    ;(plugin as any).subscriptions = [unsub]
    const spy = jest.fn()
    plugin.on('destroy', spy)
    plugin.destroy()
    expect(spy).toHaveBeenCalled()
    expect(unsub).toHaveBeenCalled()
  })
})


================================================
FILE: src/__tests__/dom.test.ts
================================================
import createElement from '../dom.js'

describe('createElement', () => {
  test('creates DOM structure', () => {
    const container = document.createElement('div')
    const el = createElement(
      'div',
      {
        id: 'root',
        children: {
          span: { textContent: 'child' },
        },
      },
      container,
    )

    expect(container.firstChild).toBe(el)
    expect((el as HTMLElement).id).toBe('root')
    expect(el.querySelector('span')?.textContent).toBe('child')
  })
})


================================================
FILE: src/__tests__/draggable.test.ts
================================================
import { makeDraggable } from '../draggable.js'

describe('makeDraggable', () => {
  beforeAll(() => {
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: jest.fn().mockReturnValue({
        matches: false,
        addListener: jest.fn(),
        removeListener: jest.fn(),
      }),
    })
    if (typeof window.PointerEvent === 'undefined') {
      class FakePointerEvent extends MouseEvent {
        constructor(type: string, props: any) {
          super(type, props)
        }
      }
      // @ts-expect-error
      window.PointerEvent = FakePointerEvent
      // @ts-expect-error
      global.PointerEvent = FakePointerEvent
    }
  })
  test('invokes callbacks on drag', () => {
    const el = document.createElement('div')
    document.body.appendChild(el)
    jest.spyOn(el, 'getBoundingClientRect').mockReturnValue({
      left: 0,
      top: 0,
      width: 100,
      height: 100,
      right: 100,
      bottom: 100,
      x: 0,
      y: 0,
      toJSON: () => {},
    })
    const onDrag = jest.fn()
    const onStart = jest.fn()
    const onEnd = jest.fn()
    const unsubscribe = makeDraggable(el, onDrag, onStart, onEnd, 0)

    el.dispatchEvent(new PointerEvent('pointerdown', { clientX: 10, clientY: 10 }))
    document.dispatchEvent(new PointerEvent('pointermove', { clientX: 20, clientY: 20 }))
    document.dispatchEvent(new PointerEvent('pointerup', { clientX: 20, clientY: 20 }))

    expect(onStart).toHaveBeenCalled()
    expect(onDrag).toHaveBeenCalled()
    expect(onEnd).toHaveBeenCalled()

    unsubscribe()
  })
})


================================================
FILE: src/__tests__/event-emitter.test.ts
================================================
import EventEmitter from '../event-emitter.js'

interface Events {
  foo: [number]
  bar: []
  [key: string]: unknown[]
}

describe('EventEmitter', () => {
  test('on and emit', () => {
    const emitter = new EventEmitter<Events>()
    const handler = jest.fn()
    emitter.on('foo', handler)
    ;(emitter as any).emit('foo', 42)
    expect(handler).toHaveBeenCalledWith(42)
  })

  test('once', () => {
    const emitter = new EventEmitter<Events>()
    const handler = jest.fn()
    emitter.once('bar', handler)
    ;(emitter as any).emit('bar')
    ;(emitter as any).emit('bar')
    expect(handler).toHaveBeenCalledTimes(1)
  })

  test('unAll', () => {
    const emitter = new EventEmitter<Events>()
    const handler = jest.fn()
    emitter.on('foo', handler)
    emitter.unAll()
    ;(emitter as any).emit('foo', 1)
    expect(handler).not.toHaveBeenCalled()
  })
})


================================================
FILE: src/__tests__/fetcher.test.ts
================================================
import Fetcher from '../fetcher.js'
import { TextEncoder } from 'util'
import { Blob as NodeBlob } from 'buffer'

describe('Fetcher', () => {
  test('fetchBlob returns blob and reports progress', async () => {
    const data = 'hello'
    const reader = {
      read: jest
        .fn()
        .mockResolvedValueOnce({ done: false, value: new TextEncoder().encode(data) })
        .mockResolvedValueOnce({ done: true, value: undefined }),
    }
    const response = {
      status: 200,
      statusText: 'OK',
      headers: new Headers({ 'Content-Length': data.length.toString() }),
      body: { getReader: () => reader },
      clone() {
        return this
      },
      blob: async () => new NodeBlob([data]),
    } as unknown as Response

    global.fetch = jest.fn().mockResolvedValue(response)

    const progress = jest.fn()
    const blob = await Fetcher.fetchBlob('url', progress)
    expect(await blob.text()).toBe(data)

    // wait for watchProgress to process
    await new Promise(process.nextTick)
    expect(progress).toHaveBeenCalledWith(100)
  })
})


================================================
FILE: src/__tests__/memory-leaks.test.ts
================================================
/**
 * Memory Leak Detection Tests
 *
 * These tests verify that WaveSurfer properly cleans up resources
 * and doesn't leak memory when destroyed and recreated multiple times.
 */

import WaveSurfer from '../wavesurfer.js'
import RegionsPlugin from '../plugins/regions.js'

// Mock audio context and matchMedia
beforeAll(() => {
  global.AudioContext = jest.fn().mockImplementation(() => ({
    createMediaElementSource: jest.fn(() => ({
      connect: jest.fn(),
      disconnect: jest.fn(),
    })),
    createGain: jest.fn(() => ({
      connect: jest.fn(),
      disconnect: jest.fn(),
      gain: { value: 1, setValueAtTime: jest.fn() },
    })),
    destination: {},
    close: jest.fn(),
  }))

  // Mock matchMedia for drag-stream
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: jest.fn().mockImplementation((query) => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(),
      removeListener: jest.fn(),
      addEventListener: jest.fn(),
      removeEventListener: jest.fn(),
      dispatchEvent: jest.fn(),
    })),
  })
})

describe('Memory Leak Detection', () => {
  let container: HTMLElement

  beforeEach(() => {
    container = document.createElement('div')
    container.id = 'waveform'
    document.body.appendChild(container)
  })

  afterEach(() => {
    document.body.removeChild(container)
  })

  describe('WaveSurfer lifecycle', () => {
    it('should cleanup subscriptions on destroy', () => {
      const ws = WaveSurfer.create({ container })

      // Track if cleanup functions are called
      const cleanupSpy = jest.fn()

      // Access internal state to verify cleanup
      const originalDestroy = ws.destroy.bind(ws)
      ws.destroy = () => {
        cleanupSpy()
        originalDestroy()
      }

      ws.destroy()

      expect(cleanupSpy).toHaveBeenCalled()
    })

    it('should not leak memory after multiple create/destroy cycles', () => {
      const instances: WaveSurfer[] = []

      // Create and destroy multiple instances
      for (let i = 0; i < 10; i++) {
        const ws = WaveSurfer.create({ container })
        instances.push(ws)
        ws.destroy()
      }

      // All instances should be destroyed
      instances.forEach((ws) => {
        // After destroy, the instance should not have active listeners
        expect(ws).toBeDefined()
      })
    })

    it('should remove all event listeners on destroy', () => {
      const ws = WaveSurfer.create({ container })

      const clickHandler = jest.fn()
      const timeUpdateHandler = jest.fn()

      ws.on('click', clickHandler)
      ws.on('timeupdate', timeUpdateHandler)

      ws.destroy()

      // After destroy, handlers should be removed
      // We can't test emit directly as it's protected, but we verified
      // the cleanup happened via destroy()
      expect(clickHandler).not.toHaveBeenCalled()
      expect(timeUpdateHandler).not.toHaveBeenCalled()
    })

    it('should cleanup DOM elements on destroy', () => {
      const ws = WaveSurfer.create({ container })

      const childCountBefore = container.children.length
      expect(childCountBefore).toBeGreaterThan(0)

      ws.destroy()

      const childCountAfter = container.children.length
      expect(childCountAfter).toBe(0)
    })

    it('should cleanup reactive subscriptions on destroy', () => {
      const ws = WaveSurfer.create({ container })

      // Get state to check reactive cleanup
      const state = ws.getState()

      // State should have reactive signals
      expect(state).toBeDefined()
      expect(state.isPlaying).toBeDefined()
      expect(state.currentTime).toBeDefined()

      ws.destroy()

      // After destroy, reactive subscriptions should be cleaned up
      expect(state).toBeDefined()
    })
  })

  describe('Plugin lifecycle', () => {
    it('should track registered plugins', () => {
      const ws = WaveSurfer.create({ container })

      // WaveSurfer should start with no plugins
      expect(ws).toBeDefined()

      ws.destroy()
    })

    it('should remove plugin elements from DOM on destroy', () => {
      WaveSurfer.create({ container })

      // Mock a plugin that adds DOM elements
      const pluginElement = document.createElement('div')
      pluginElement.className = 'test-plugin'
      container.appendChild(pluginElement)

      const elementCountBefore = container.querySelectorAll('.test-plugin').length
      expect(elementCountBefore).toBe(1)

      // Plugin should cleanup its elements
      pluginElement.remove()

      const elementCountAfter = container.querySelectorAll('.test-plugin').length
      expect(elementCountAfter).toBe(0)
    })
  })

  describe('Regions plugin memory leak (#4243)', () => {
    it('should cleanup region event listeners when removed', () => {
      const ws = WaveSurfer.create({ container })
      const regions = ws.registerPlugin(RegionsPlugin.create())

      // Mock duration so regions are saved immediately
      jest.spyOn(ws, 'getDuration').mockReturnValue(10)
      jest.spyOn(ws, 'getDecodedData').mockReturnValue({ numberOfChannels: 1 } as any)

      // Create a region
      const region = regions.addRegion({ start: 0, end: 1 })

      // Track if cleanup is happening
      const clickHandler = jest.fn()
      region.on('click', clickHandler)

      // Remove the region
      region.remove()

      // After removal, the region element should be null
      expect(region.element).toBeNull()

      // Cleanup
      ws.destroy()
    })

    it('should not retain 
Download .txt
gitextract_apdo23lc/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   └── question.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── build/
│       │   └── action.yml
│       ├── e2e.yml
│       ├── label-sponsors.yml
│       ├── lint.yml
│       ├── release.yml
│       ├── unit-tests.yml
│       └── yarn/
│           └── action.yml
├── .gitignore
├── .prettierrc
├── AGENTS.md
├── AI_OVERVIEW.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cypress/
│   ├── e2e/
│   │   ├── abort.cy.js
│   │   ├── basic.cy.js
│   │   ├── envelope.cy.js
│   │   ├── error.cy.js
│   │   ├── hover.cy.js
│   │   ├── index.html
│   │   ├── options.cy.js
│   │   ├── regions-no-audio.cy.js
│   │   ├── regions.cy.js
│   │   ├── spectrogram.cy.js
│   │   ├── umd.cy.js
│   │   ├── umd.html
│   │   └── webaudio.cy.js
│   └── support/
│       ├── commands.ts
│       └── e2e.ts
├── cypress.config.js
├── eslint.config.js
├── examples/
│   ├── _preview.js
│   ├── all-options.js
│   ├── audio/
│   │   └── reed.sh
│   ├── bars.js
│   ├── basic.js
│   ├── custom-render.js
│   ├── envelope.js
│   ├── events.js
│   ├── fm-synth.js
│   ├── gradient.js
│   ├── hover.js
│   ├── minimap.js
│   ├── multitrack.js
│   ├── phase-vocoder/
│   │   └── index.js
│   ├── pitch-worker.js
│   ├── pitch.js
│   ├── predecoded.js
│   ├── react-global-player.js
│   ├── react.js
│   ├── record-sync.js
│   ├── record.js
│   ├── regions.js
│   ├── silence.js
│   ├── soundcloud.js
│   ├── spectrogram-windowed.js
│   ├── spectrogram.js
│   ├── speed.js
│   ├── split-channels.js
│   ├── styling.js
│   ├── timeline-custom.js
│   ├── timeline.js
│   ├── video.js
│   ├── vowels.js
│   ├── webaudio-shim.js
│   ├── webaudio.js
│   ├── zoom-plugin.js
│   └── zoom.js
├── index.html
├── jest.config.js
├── package.json
├── rollup.config.js
├── scripts/
│   ├── clean.cjs
│   ├── plugin.sh
│   └── plugin.ts.template
├── src/
│   ├── __tests__/
│   │   ├── base-plugin.test.ts
│   │   ├── dom.test.ts
│   │   ├── draggable.test.ts
│   │   ├── event-emitter.test.ts
│   │   ├── fetcher.test.ts
│   │   ├── memory-leaks.test.ts
│   │   ├── minimap.test.ts
│   │   ├── player.test.ts
│   │   ├── regions.test.ts
│   │   ├── renderer-utils.test.ts
│   │   ├── renderer.test.ts
│   │   ├── timeline.test.ts
│   │   ├── timer.test.ts
│   │   └── wavesurfer.test.ts
│   ├── base-plugin.ts
│   ├── decoder.ts
│   ├── dom.ts
│   ├── draggable.ts
│   ├── event-emitter.ts
│   ├── fetcher.ts
│   ├── fft.ts
│   ├── player.ts
│   ├── plugins/
│   │   ├── envelope.ts
│   │   ├── hover.ts
│   │   ├── minimap.ts
│   │   ├── record.ts
│   │   ├── regions.ts
│   │   ├── spectrogram-windowed.ts
│   │   ├── spectrogram-worker.ts
│   │   ├── spectrogram.ts
│   │   ├── timeline.ts
│   │   └── zoom.ts
│   ├── reactive/
│   │   ├── README.md
│   │   ├── __tests__/
│   │   │   ├── drag-stream.test.ts
│   │   │   ├── event-stream-emitter.test.ts
│   │   │   ├── event-streams.test.ts
│   │   │   ├── media-event-bridge.test.ts
│   │   │   ├── render-scheduler.test.ts
│   │   │   ├── scroll-stream.test.ts
│   │   │   ├── state-event-emitter.test.ts
│   │   │   └── store.test.ts
│   │   ├── drag-stream.ts
│   │   ├── event-stream-emitter.ts
│   │   ├── event-streams.ts
│   │   ├── media-event-bridge.ts
│   │   ├── render-scheduler.ts
│   │   ├── scroll-stream.ts
│   │   ├── state-event-emitter.ts
│   │   └── store.ts
│   ├── renderer-utils.ts
│   ├── renderer.ts
│   ├── state/
│   │   ├── __tests__/
│   │   │   └── wavesurfer-state.test.ts
│   │   └── wavesurfer-state.ts
│   ├── timer.ts
│   ├── wavesurfer.ts
│   └── webaudio.ts
├── tsconfig.json
└── tsconfig.test.json
Download .txt
SYMBOL INDEX (505 symbols across 46 files)

FILE: cypress.config.js
  method setupNodeEvents (line 7) | setupNodeEvents(on, config) {

FILE: examples/fm-synth.js
  function createVoice (line 21) | function createVoice() {
  function playNote (line 59) | function playNote(frequency, modulationFrequency, modulationDepth, durat...
  function releaseNote (line 73) | function releaseNote(voice, duration) {
  function createPianoRoll (line 93) | function createPianoRoll() {
  function randomizeFmParams (line 188) | function randomizeFmParams() {
  function drawWaveform (line 195) | function drawWaveform() {
  function animate (line 202) | function animate() {

FILE: src/__tests__/base-plugin.test.ts
  class TestPlugin (line 3) | class TestPlugin extends BasePlugin<{ destroy: [] }, {}> {
    method onInit (line 5) | protected onInit() {

FILE: src/__tests__/draggable.test.ts
  class FakePointerEvent (line 14) | class FakePointerEvent extends MouseEvent {
    method constructor (line 15) | constructor(type: string, props: any) {

FILE: src/__tests__/event-emitter.test.ts
  type Events (line 3) | interface Events {

FILE: src/__tests__/fetcher.test.ts
  method clone (line 19) | clone() {

FILE: src/__tests__/minimap.test.ts
  type Listener (line 11) | type Listener = (...args: any[]) => void

FILE: src/__tests__/player.test.ts
  type Events (line 3) | interface Events {

FILE: src/__tests__/regions.test.ts
  type Listener (line 3) | type Listener = (...args: any[]) => void

FILE: src/__tests__/renderer.test.ts
  type Window (line 4) | interface Window {

FILE: src/__tests__/timeline.test.ts
  type Listener (line 4) | type Listener = (...args: any[]) => void

FILE: src/__tests__/wavesurfer.test.ts
  class Renderer (line 3) | class Renderer {
    method constructor (line 18) | constructor(options: any) {
  class Timer (line 28) | class Timer {
  class TestPlugin (line 91) | class TestPlugin extends BasePlugin<{ destroy: [] }, {}> {}

FILE: src/base-plugin.ts
  type BasePluginEvents (line 4) | type BasePluginEvents = {
  type GenericPlugin (line 8) | type GenericPlugin = BasePlugin<BasePluginEvents, unknown>
  class BasePlugin (line 11) | class BasePlugin<EventTypes extends BasePluginEvents, Options> extends E...
    method constructor (line 18) | constructor(options: Options) {
    method onInit (line 24) | protected onInit() {
    method _init (line 29) | public _init(wavesurfer: WaveSurfer) {
    method destroy (line 41) | public destroy() {

FILE: src/decoder.ts
  function decode (line 2) | async function decode(audioData: ArrayBuffer, sampleRate: number): Promi...
  function normalize (line 13) | function normalize<T extends Array<Float32Array | number[]>>(channelData...
  function createBuffer (line 32) | function createBuffer(channelData: Array<Float32Array | number[]>, durat...

FILE: src/dom.ts
  type TreeNode (line 1) | type TreeNode = { [key: string]: string | number | boolean | CSSStyleDec...
  function renderNode (line 8) | function renderNode(tagName: string, content: TreeNode): HTMLElement | S...
  function createElement (line 38) | function createElement(tagName: string, content?: TreeNode, container?: ...

FILE: src/draggable.ts
  function makeDraggable (line 5) | function makeDraggable(

FILE: src/event-emitter.ts
  type GeneralEventTypes (line 1) | type GeneralEventTypes = {
  type EventListener (line 7) | type EventListener<EventTypes extends GeneralEventTypes, EventName exten...
  type EventMap (line 11) | type EventMap<EventTypes extends GeneralEventTypes> = {
  class EventEmitter (line 16) | class EventEmitter<EventTypes extends GeneralEventTypes> {
    method on (line 20) | public on<EventName extends keyof EventTypes>(
    method un (line 44) | public un<EventName extends keyof EventTypes>(
    method once (line 52) | public once<EventName extends keyof EventTypes>(
    method unAll (line 60) | public unAll(): void {
    method emit (line 65) | protected emit<EventName extends keyof EventTypes>(eventName: EventNam...

FILE: src/fetcher.ts
  function watchProgress (line 1) | async function watchProgress(response: Response, progressCallback: (perc...
  function fetchBlob (line 33) | async function fetchBlob(

FILE: src/fft.ts
  constant ERB_A (line 11) | const ERB_A = (1000 * Math.log(10)) / (24.7 * 4.37)
  function hzToMel (line 14) | function hzToMel(hz: number): number {
  function melToHz (line 18) | function melToHz(mel: number): number {
  function hzToLog (line 22) | function hzToLog(hz: number): number {
  function logToHz (line 26) | function logToHz(log: number): number {
  function hzToBark (line 30) | function hzToBark(hz: number): number {
  function barkToHz (line 42) | function barkToHz(bark: number): number {
  function hzToErb (line 53) | function hzToErb(hz: number): number {
  function erbToHz (line 58) | function erbToHz(erb: number): number {
  function hzToScale (line 64) | function hzToScale(hz: number, scale: 'linear' | 'logarithmic' | 'mel' |...
  function scaleToHz (line 79) | function scaleToHz(scale: number, scaleType: 'linear' | 'logarithmic' | ...
  function createFilterBank (line 95) | function createFilterBank(
  function applyFilterBank (line 119) | function applyFilterBank(fftPoints: Float32Array, filterBank: number[][]...
  function createFilterBankForScale (line 131) | function createFilterBankForScale(
  constant COLOR_MAPS (line 152) | const COLOR_MAPS = {
  function setupColorMap (line 434) | function setupColorMap(colorMap: number[][] | 'gray' | 'igray' | 'roseus...
  function freqType (line 459) | function freqType(freq: number): string {
  function unitType (line 466) | function unitType(freq: number): string {
  function getLabelFrequency (line 473) | function getLabelFrequency(
  function createWrapperClickHandler (line 488) | function createWrapperClickHandler(wrapper: HTMLElement, emit: (event: s...
  function FFT (line 501) | function FFT(bufferSize: number, sampleRate: number, windowFunc: string,...
  class FFT (line 686) | class FFT {

FILE: src/player.ts
  type PlayerOptions (line 4) | type PlayerOptions = {
  class Player (line 11) | class Player<T extends GeneralEventTypes> extends EventEmitter<T> {
    method isPlayingSignal (line 27) | public get isPlayingSignal(): WritableSignal<boolean> {
    method currentTimeSignal (line 30) | public get currentTimeSignal(): WritableSignal<number> {
    method durationSignal (line 33) | public get durationSignal(): WritableSignal<number> {
    method volumeSignal (line 36) | public get volumeSignal(): WritableSignal<number> {
    method mutedSignal (line 39) | public get mutedSignal(): WritableSignal<boolean> {
    method playbackRateSignal (line 42) | public get playbackRateSignal(): WritableSignal<number> {
    method seekingSignal (line 45) | public get seekingSignal(): WritableSignal<boolean> {
    method constructor (line 49) | constructor(options: PlayerOptions) {
    method setupReactiveMediaEvents (line 97) | private setupReactiveMediaEvents() {
    method onMediaEvent (line 165) | protected onMediaEvent<K extends keyof HTMLElementEventMap>(
    method getSrc (line 174) | protected getSrc() {
    method revokeSrc (line 178) | private revokeSrc() {
    method canPlayType (line 185) | private canPlayType(type: string): boolean {
    method setSrc (line 189) | protected setSrc(url: string, blob?: Blob) {
    method destroy (line 210) | protected destroy() {
    method setMediaElement (line 225) | protected setMediaElement(element: HTMLMediaElement) {
    method play (line 238) | public async play(): Promise<void> {
    method pause (line 250) | public pause(): void {
    method isPlaying (line 255) | public isPlaying(): boolean {
    method setTime (line 260) | public setTime(time: number) {
    method getDuration (line 265) | public getDuration(): number {
    method getCurrentTime (line 270) | public getCurrentTime(): number {
    method getVolume (line 275) | public getVolume(): number {
    method setVolume (line 280) | public setVolume(volume: number) {
    method getMuted (line 285) | public getMuted(): boolean {
    method setMuted (line 290) | public setMuted(muted: boolean) {
    method getPlaybackRate (line 295) | public getPlaybackRate(): number {
    method isSeeking (line 300) | public isSeeking(): boolean {
    method setPlaybackRate (line 305) | public setPlaybackRate(rate: number, preservePitch?: boolean) {
    method getMediaElement (line 314) | public getMediaElement(): HTMLMediaElement {
    method setSinkId (line 319) | public setSinkId(sinkId: string): Promise<void> {

FILE: src/plugins/envelope.ts
  type EnvelopePoint (line 11) | type EnvelopePoint = {
  type EnvelopePluginOptions (line 17) | type EnvelopePluginOptions = {
  type Options (line 37) | type Options = EnvelopePluginOptions & typeof defaultOptions
  type EnvelopePluginEvents (line 39) | type EnvelopePluginEvents = BasePluginEvents & {
  class Polyline (line 44) | class Polyline extends EventEmitter<{
    method constructor (line 66) | constructor(options: Options, wrapper: HTMLElement) {
    method makeDraggable (line 186) | private makeDraggable(draggable: SVGElement, onDrag: (x: number, y: nu...
    method createCircle (line 208) | private createCircle(x: number, y: number) {
    method removePolyPoint (line 232) | removePolyPoint(point: EnvelopePoint) {
    method addPolyPoint (line 243) | addPolyPoint(relX: number, relY: number, refPoint: EnvelopePoint) {
    method update (line 289) | update() {
    method destroy (line 310) | destroy() {
  class EnvelopePlugin (line 343) | class EnvelopePlugin extends BasePlugin<EnvelopePluginEvents, EnvelopePl...
    method constructor (line 353) | constructor(options: EnvelopePluginOptions) {
    method create (line 365) | public static create(options: EnvelopePluginOptions) {
    method addPoint (line 372) | public addPoint(point: EnvelopePoint) {
    method removePoint (line 391) | public removePoint(point: EnvelopePoint) {
    method getPoints (line 403) | public getPoints(): EnvelopePoint[] {
    method setPoints (line 410) | public setPoints(newPoints: EnvelopePoint[]) {
    method destroy (line 418) | public destroy() {
    method getCurrentVolume (line 432) | public getCurrentVolume(): number {
    method setVolume (line 439) | public setVolume(floatValue: number) {
    method onInit (line 445) | onInit() {
    method emitPoints (line 474) | private emitPoints() {
    method initPolyline (line 483) | private initPolyline() {
    method addPolyPoint (line 523) | private addPolyPoint(point: EnvelopePoint, duration: number) {
    method onTimeUpdate (line 527) | private onTimeUpdate(time: number) {

FILE: src/plugins/hover.ts
  type HoverPluginOptions (line 10) | type HoverPluginOptions = {
  method formatTimeCallback (line 41) | formatTimeCallback(seconds: number) {
  type HoverPluginEvents (line 49) | type HoverPluginEvents = BasePluginEvents & {
  class HoverPlugin (line 53) | class HoverPlugin extends BasePlugin<HoverPluginEvents, HoverPluginOptio...
    method constructor (line 59) | constructor(options?: HoverPluginOptions) {
    method create (line 68) | public static create(options?: HoverPluginOptions) {
    method addUnits (line 72) | private addUnits(value: string | number): string {
    method onInit (line 78) | onInit() {
    method destroy (line 187) | public destroy() {

FILE: src/plugins/minimap.ts
  type MinimapPluginOptions (line 9) | type MinimapPluginOptions = {
  type MinimapPluginEvents (line 20) | type MinimapPluginEvents = BasePluginEvents & {
  class MinimapPlugin (line 51) | class MinimapPlugin extends BasePlugin<MinimapPluginEvents, MinimapPlugi...
    method constructor (line 61) | constructor(options: MinimapPluginOptions) {
    method create (line 69) | public static create(options: MinimapPluginOptions) {
    method onInit (line 74) | onInit() {
    method initMinimapWrapper (line 98) | private initMinimapWrapper(): HTMLElement {
    method initOverlay (line 107) | private initOverlay(): HTMLElement {
    method initMinimap (line 127) | private initMinimap() {
    method getOverlayWidth (line 232) | private getOverlayWidth(): number {
    method destroyMinimap (line 237) | private destroyMinimap() {
    method renderMainProgress (line 250) | private renderMainProgress(progress: number) {
    method renderMinimapProgress (line 255) | private renderMinimapProgress(progress: number) {
    method syncMinimapPosition (line 260) | private syncMinimapPosition(currentTime: number) {
    method onMinimapDrag (line 273) | private onMinimapDrag(relativeX: number) {
    method onRedraw (line 297) | private onRedraw() {
    method onScroll (line 302) | private onScroll(startTime: number) {
    method initWaveSurferEvents (line 308) | private initWaveSurferEvents() {
    method destroy (line 336) | public destroy() {

FILE: src/plugins/record.ts
  type RecordPluginOptions (line 9) | type RecordPluginOptions = {
  type RecordPluginDeviceOptions (line 28) | type RecordPluginDeviceOptions = MediaTrackConstraints
  type RecordPluginEvents (line 30) | type RecordPluginEvents = BasePluginEvents & {
  type MicStream (line 45) | type MicStream = {
  constant DEFAULT_BITS_PER_SECOND (line 50) | const DEFAULT_BITS_PER_SECOND = 128000
  constant DEFAULT_SCROLLING_WAVEFORM_WINDOW (line 51) | const DEFAULT_SCROLLING_WAVEFORM_WINDOW = 5
  constant FPS (line 52) | const FPS = 100
  constant MIME_TYPES (line 54) | const MIME_TYPES = ['audio/webm', 'audio/wav', 'audio/mpeg', 'audio/mp4'...
  class RecordPlugin (line 57) | class RecordPlugin extends BasePlugin<RecordPluginEvents, RecordPluginOp...
    method constructor (line 73) | constructor(options: RecordPluginOptions) {
    method create (line 96) | public static create(options?: RecordPluginOptions) {
    method renderMicStream (line 100) | public renderMicStream(stream: MediaStream): MicStream {
    method startMic (line 236) | public async startMic(options?: RecordPluginDeviceOptions): Promise<Me...
    method stopMic (line 261) | public stopMic() {
    method startRecording (line 275) | public async startRecording(options?: RecordPluginDeviceOptions) {
    method getDuration (line 325) | public getDuration(): number {
    method isRecording (line 330) | public isRecording(): boolean {
    method isPaused (line 334) | public isPaused(): boolean {
    method isActive (line 338) | public isActive(): boolean {
    method stopRecording (line 343) | public stopRecording() {
    method pauseRecording (line 351) | public pauseRecording() {
    method resumeRecording (line 362) | public resumeRecording() {
    method getAvailableAudioDevices (line 377) | public static async getAvailableAudioDevices() {
    method destroy (line 384) | public destroy() {
    method applyOriginalOptionsIfNeeded (line 396) | private applyOriginalOptionsIfNeeded() {

FILE: src/plugins/regions.ts
  type RegionsPluginOptions (line 14) | type RegionsPluginOptions = undefined
  type UpdateSide (line 15) | type UpdateSide = 'start' | 'end'
  type RegionsPluginEvents (line 16) | type RegionsPluginEvents = BasePluginEvents & {
  type RegionEvents (line 39) | type RegionEvents = {
  type RegionParams (line 62) | type RegionParams = {
  class SingleRegion (line 91) | class SingleRegion extends EventEmitter<RegionEvents> implements Region {
    method constructor (line 112) | constructor(
    method clampPosition (line 140) | private clampPosition(time: number): number {
    method setPart (line 144) | private setPart() {
    method addResizeHandles (line 149) | private addResizeHandles(element: HTMLElement) {
    method removeResizeHandles (line 221) | private removeResizeHandles(element: HTMLElement) {
    method initElement (line 232) | private initElement(): HTMLElement | null {
    method renderPosition (line 268) | private renderPosition() {
    method toggleCursor (line 276) | private toggleCursor(toggle: boolean) {
    method initMouseEvents (line 281) | private initMouseEvents() {
    method _onUpdate (line 347) | public _onUpdate(dx: number, side?: UpdateSide, startTime?: number) {
    method onMove (line 379) | private onMove(dx: number) {
    method onResize (line 384) | private onResize(dx: number, side: UpdateSide) {
    method onEndResizing (line 391) | private onEndResizing(side: UpdateSide) {
    method onContentClick (line 397) | private onContentClick(event: MouseEvent) {
    method onContentBlur (line 404) | public onContentBlur() {
    method _setTotalDuration (line 408) | public _setTotalDuration(totalDuration: number) {
    method play (line 414) | public play(stopAtEnd?: boolean) {
    method getContent (line 419) | public getContent(asHTML: boolean = false): string | HTMLElement | und...
    method setContent (line 430) | public setContent(content: RegionParams['content']) {
    method setOptions (line 474) | public setOptions(
    method remove (line 529) | public remove() {
  class RegionsPlugin (line 560) | class RegionsPlugin extends BasePlugin<RegionsPluginEvents, RegionsPlugi...
    method constructor (line 565) | constructor(options?: RegionsPluginOptions) {
    method create (line 571) | public static create(options?: RegionsPluginOptions) {
    method onInit (line 576) | onInit() {
    method initRegionsContainer (line 619) | private initRegionsContainer(): HTMLElement {
    method getRegions (line 635) | public getRegions(): Region[] {
    method avoidOverlapping (line 639) | private avoidOverlapping(region: Region) {
    method adjustScroll (line 670) | private adjustScroll(region: Region) {
    method virtualAppend (line 687) | private virtualAppend(region: Region, container: HTMLElement, element:...
    method saveRegion (line 730) | private saveRegion(region: Region) {
    method addRegion (line 779) | public addRegion(options: RegionParams): Region {
    method enableDragSelection (line 807) | public enableDragSelection(options: Omit<RegionParams, 'start' | 'end'...
    method clearRegions (line 877) | public clearRegions() {
    method destroy (line 884) | public destroy() {
  type Region (line 892) | type Region = SingleRegion

FILE: src/plugins/spectrogram-windowed.ts
  type WindowedSpectrogramPluginOptions (line 28) | type WindowedSpectrogramPluginOptions = {
  type WindowedSpectrogramPluginEvents (line 82) | type WindowedSpectrogramPluginEvents = BasePluginEvents & {
  type FrequencySegment (line 91) | interface FrequencySegment {
  class WindowedSpectrogramPlugin (line 100) | class WindowedSpectrogramPlugin extends BasePlugin<WindowedSpectrogramPl...
    method create (line 145) | static create(options?: WindowedSpectrogramPluginOptions) {
    method constructor (line 149) | constructor(options: WindowedSpectrogramPluginOptions) {
    method initializeWorker (line 195) | private initializeWorker() {
    method onInit (line 233) | onInit() {
    method createWrapper (line 292) | private createWrapper() {
    method createCanvas (line 323) | private createCanvas() {
    method handleRedraw (line 340) | private handleRedraw() {
    method updateSegmentPositions (line 352) | private updateSegmentPositions(oldPxPerSec: number, newPxPerSec: numbe...
    method scheduleSegmentQualityUpdate (line 373) | private scheduleSegmentQualityUpdate() {
    method updateVisibleSegmentQuality (line 386) | private async updateVisibleSegmentQuality() {
    method getScrollLeft (line 417) | private getScrollLeft(wrapper: HTMLElement): number {
    method getViewportWidth (line 439) | private getViewportWidth(wrapper: HTMLElement): number {
    method handleScroll (line 449) | private handleScroll() {
    method updatePosition (line 484) | private updatePosition(currentTime: number) {
    method scheduleRender (line 489) | private scheduleRender() {
    method renderVisibleWindow (line 498) | private async renderVisibleWindow() {
    method generateSegments (line 536) | private async generateSegments(startTime: number, endTime: number) {
    method findUncoveredTimeRanges (line 632) | private findUncoveredTimeRanges(
    method startProgressiveLoading (line 673) | private startProgressiveLoading() {
    method progressiveLoadNextSegment (line 685) | private async progressiveLoadNextSegment() {
    method _stopProgressiveLoading (line 724) | private _stopProgressiveLoading() {
    method getLoadingProgress (line 733) | public getLoadingProgress(): number {
    method emitProgress (line 752) | private emitProgress() {
    method calculateFrequencies (line 757) | private async calculateFrequencies(startTime: number, endTime: number)...
    method calculateFrequenciesWithWorker (line 780) | private async calculateFrequenciesWithWorker(startTime: number, endTim...
    method calculateFrequenciesMainThread (line 845) | private async calculateFrequenciesMainThread(startTime: number, endTim...
    method renderSegment (line 920) | private async renderSegment(segment: FrequencySegment) {
    method renderChannelToCanvas (line 961) | private async renderChannelToCanvas(
    method clearAllSegments (line 1014) | private clearAllSegments() {
    method getFilterBank (line 1023) | private getFilterBank(sampleRate: number): number[][] | null {
    method freqType (line 1036) | private freqType(freq: number) {
    method unitType (line 1040) | private unitType(freq: number) {
    method getLabelFrequency (line 1044) | private getLabelFrequency(index: number, labelIndex: number) {
    method loadLabels (line 1050) | private loadLabels(
    method render (line 1120) | async render(audioData: AudioBuffer) {
    method destroy (line 1153) | destroy() {
    method getWidth (line 1210) | private getWidth() {
    method getPixelsPerSecond (line 1214) | private getPixelsPerSecond() {
    method stopProgressiveLoading (line 1240) | public stopProgressiveLoading() {
    method restartProgressiveLoading (line 1249) | public restartProgressiveLoading() {

FILE: src/plugins/spectrogram-worker.ts
  type WorkerMessage (line 12) | interface WorkerMessage {
  type WorkerResponse (line 31) | interface WorkerResponse {
  function calculateFrequencies (line 65) | function calculateFrequencies(audioChannels: Float32Array[], options: Wo...

FILE: src/plugins/spectrogram.ts
  type SpectrogramPluginOptions (line 45) | type SpectrogramPluginOptions = {
  type SpectrogramPluginEvents (line 117) | type SpectrogramPluginEvents = BasePluginEvents & {
  class SpectrogramPlugin (line 122) | class SpectrogramPlugin extends BasePlugin<SpectrogramPluginEvents, Spec...
    method create (line 169) | static create(options?: SpectrogramPluginOptions) {
    method constructor (line 173) | constructor(options: SpectrogramPluginOptions) {
    method initializeWorker (line 231) | private initializeWorker() {
    method onInit (line 269) | onInit() {
    method destroy (line 301) | public destroy() {
    method loadFrequenciesData (line 368) | public async loadFrequenciesData(url: string | URL) {
    method getFrequenciesData (line 377) | public async getFrequenciesData(): Uint8Array[][] | null {
    method clearCache (line 396) | public clearCache() {
    method createWrapper (line 404) | private createWrapper() {
    method createCanvas (line 435) | private createCanvas() {
    method createSingleCanvas (line 452) | private createSingleCanvas(width: number, height: number, offset: numb...
    method clearCanvases (line 471) | private clearCanvases() {
    method clearExcessCanvases (line 477) | private clearExcessCanvases() {
    method throttledRender (line 484) | private throttledRender() {
    method render (line 512) | private async render() {
    method fastRender (line 532) | private fastRender() {
    method drawSpectrogram (line 545) | private drawSpectrogram(frequenciesData: Uint8Array[][]): void {
    method drawSpectrogramSegment (line 718) | private drawSpectrogramSegment(
    method getWidth (line 792) | private getWidth() {
    method getWrapperWidth (line 796) | private getWrapperWidth() {
    method calculateFrequenciesWithWorker (line 800) | private async calculateFrequenciesWithWorker(buffer: AudioBuffer): Pro...
    method getFrequencies (line 862) | private async getFrequencies(buffer: AudioBuffer): Promise<Uint8Array[...
    method loadLabels (line 946) | private loadLabels(
    method efficientResample (line 1017) | private efficientResample(frequenciesData: Uint8Array[][], targetWidth...
    method resampleChannel (line 1021) | private resampleChannel(oldMatrix: Uint8Array[], targetWidth: number):...
    method fillImageDataQuality (line 1088) | private fillImageDataQuality(

FILE: src/plugins/timeline.ts
  type TimelinePluginOptions (line 9) | type TimelinePluginOptions = {
  type TimelinePluginEvents (line 54) | type TimelinePluginEvents = BasePluginEvents & {
  class TimelinePlugin (line 58) | class TimelinePlugin extends BasePlugin<TimelinePluginEvents, TimelinePl...
    method constructor (line 64) | constructor(options?: TimelinePluginOptions) {
    method create (line 71) | public static create(options?: TimelinePluginOptions) {
    method onInit (line 76) | onInit() {
    method destroy (line 129) | public destroy() {
    method initTimelineWrapper (line 134) | private initTimelineWrapper(): HTMLElement {
    method defaultTimeInterval (line 139) | private defaultTimeInterval(pxPerSec: number): number {
    method defaultPrimaryLabelInterval (line 151) | private defaultPrimaryLabelInterval(pxPerSec: number): number {
    method defaultSecondaryLabelInterval (line 163) | private defaultSecondaryLabelInterval(pxPerSec: number): number {
    method virtualAppend (line 174) | private virtualAppend(start: number, container: HTMLElement, element: ...
    method updateVisibleNotches (line 196) | private updateVisibleNotches(scrollLeft: number, scrollRight: number, ...
    method initTimeline (line 211) | private initTimeline() {

FILE: src/plugins/zoom.ts
  type ZoomPluginOptions (line 28) | type ZoomPluginOptions = {
  type ZoomPluginEvents (line 65) | type ZoomPluginEvents = BasePluginEvents
  class ZoomPlugin (line 67) | class ZoomPlugin extends BasePlugin<ZoomPluginEvents, ZoomPluginOptions> {
    method constructor (line 84) | constructor(options?: ZoomPluginOptions) {
    method create (line 89) | public static create(options?: ZoomPluginOptions) {
    method onInit (line 93) | onInit() {
    method getTouchDistance (line 230) | private getTouchDistance(e: TouchEvent): number {
    method getTouchCenterX (line 236) | private getTouchCenterX(e: TouchEvent): number {
    method destroy (line 308) | destroy() {

FILE: src/reactive/__tests__/drag-stream.test.ts
  class FakePointerEvent (line 17) | class FakePointerEvent extends MouseEvent {
    method constructor (line 18) | constructor(type: string, props: any) {

FILE: src/reactive/__tests__/event-stream-emitter.test.ts
  type TestEvents (line 4) | type TestEvents = {
  class TestEmitter (line 13) | class TestEmitter extends EventEmitter<TestEvents> {
    method emit (line 14) | public emit<E extends keyof TestEvents>(event: E, ...args: TestEvents[...

FILE: src/reactive/drag-stream.ts
  type DragEvent (line 11) | interface DragEvent {
  type DragStreamOptions (line 19) | interface DragStreamOptions {
  function createDragStream (line 50) | function createDragStream(

FILE: src/reactive/event-stream-emitter.ts
  function toStream (line 32) | function toStream<T extends Record<string, any[]>, K extends keyof T>(
  function toStreams (line 76) | function toStreams<T extends Record<string, any[]>, K extends keyof T>(
  function mergeStreams (line 119) | function mergeStreams<T extends Record<string, any[]>, K extends keyof T>(
  function mapStream (line 157) | function mapStream<T, U>(source: Signal<T>, mapper: (value: T) => U): Si...
  function filterStream (line 179) | function filterStream<T>(source: Signal<T>, predicate: (value: T) => boo...

FILE: src/reactive/event-streams.ts
  function fromEvent (line 18) | function fromEvent<K extends keyof HTMLElementEventMap>(
  function map (line 47) | function map<T, U>(source: Signal<T>, mapper: (value: T) => U): Signal<U> {
  function filter (line 69) | function filter<T>(source: Signal<T>, predicate: (value: T) => boolean):...
  function debounce (line 96) | function debounce<T>(source: Signal<T>, delay: number): Signal<T> {
  function throttle (line 128) | function throttle<T>(source: Signal<T>, delay: number): Signal<T> {
  function cleanup (line 165) | function cleanup(stream: Signal<any>): void {

FILE: src/reactive/media-event-bridge.ts
  function bridgeMediaEvents (line 32) | function bridgeMediaEvents(media: HTMLMediaElement, actions: WaveSurferA...
  function bridgeMediaEventsWithHandler (line 139) | function bridgeMediaEventsWithHandler(

FILE: src/reactive/render-scheduler.ts
  type RenderPriority (line 6) | type RenderPriority = 'high' | 'normal' | 'low'
  class RenderScheduler (line 8) | class RenderScheduler {
    method scheduleRender (line 29) | scheduleRender(renderFn: () => void, priority: RenderPriority = 'norma...
    method cancelRender (line 55) | cancelRender(): void {
    method flushRender (line 69) | flushRender(renderFn: () => void): void {
    method isPending (line 77) | isPending(): boolean {

FILE: src/reactive/scroll-stream.ts
  type ScrollData (line 11) | interface ScrollData {
  type ScrollPercentages (line 20) | interface ScrollPercentages {
  function calculateScrollPercentages (line 38) | function calculateScrollPercentages(scrollData: ScrollData): ScrollPerce...
  function calculateScrollBounds (line 61) | function calculateScrollBounds(scrollData: ScrollData): { left: number; ...
  type ScrollStream (line 72) | interface ScrollStream {
  function createScrollStream (line 104) | function createScrollStream(element: HTMLElement): ScrollStream {
  function createScrollStreamWithAction (line 164) | function createScrollStreamWithAction(

FILE: src/reactive/state-event-emitter.ts
  type EventEmitter (line 11) | type EventEmitter = {
  function setupStateEventEmission (line 40) | function setupStateEventEmission(state: WaveSurferState, emitter: EventE...
  function setupSignalEventEmission (line 169) | function setupSignalEventEmission<T>(
  function setupDebouncedEventEmission (line 202) | function setupDebouncedEventEmission<T>(
  function setupConditionalEventEmission (line 257) | function setupConditionalEventEmission<T>(

FILE: src/reactive/store.ts
  type Signal (line 11) | interface Signal<T> {
  type WritableSignal (line 21) | interface WritableSignal<T> extends Signal<T> {
  function signal (line 38) | function signal<T>(initialValue: T): WritableSignal<T> {
  function computed (line 78) | function computed<T>(fn: () => T, dependencies: Signal<any>[]): Signal<T> {
  function effect (line 123) | function effect(fn: () => void | (() => void), dependencies: Signal<any>...

FILE: src/renderer-utils.ts
  type ChannelData (line 3) | type ChannelData = Array<Float32Array | number[]>
  type BarSegment (line 5) | type BarSegment = {
  type LinePath (line 12) | type LinePath = Array<{ x: number; y: number }>
  constant DEFAULT_HEIGHT (line 14) | const DEFAULT_HEIGHT = 128
  constant MAX_CANVAS_WIDTH (line 16) | const MAX_CANVAS_WIDTH = 8000
  constant MAX_NODES (line 18) | const MAX_NODES = 10
  function clampToUnit (line 20) | function clampToUnit(value: number): number {
  function calculateBarRenderConfig (line 26) | function calculateBarRenderConfig({
  function calculateBarHeights (line 58) | function calculateBarHeights({
  function resolveBarYPosition (line 87) | function resolveBarYPosition({
  function calculateBarSegments (line 105) | function calculateBarSegments({
  function getRelativePointerPosition (line 178) | function getRelativePointerPosition(rect: DOMRect, clientX: number, clie...
  function resolveChannelHeight (line 186) | function resolveChannelHeight({
  function getPixelRatio (line 212) | function getPixelRatio(devicePixelRatio?: number): number {
  function shouldRenderBars (line 216) | function shouldRenderBars(options: WaveSurferOptions): boolean {
  function resolveColorValue (line 220) | function resolveColorValue(
  function calculateWaveformLayout (line 242) | function calculateWaveformLayout({
  function clampWidthToBarGrid (line 268) | function clampWidthToBarGrid(width: number, options: WaveSurferOptions):...
  function calculateSingleCanvasWidth (line 277) | function calculateSingleCanvasWidth({
  function sliceChannelData (line 290) | function sliceChannelData({
  function shouldClearCanvases (line 308) | function shouldClearCanvases(currentNodeCount: number): boolean {
  function getLazyRenderRange (line 312) | function getLazyRenderRange({
  function calculateVerticalScale (line 327) | function calculateVerticalScale({
  function calculateLinePaths (line 358) | function calculateLinePaths({
  function calculateScrollPercentages (line 409) | function calculateScrollPercentages({
  function roundToHalfAwayFromZero (line 431) | function roundToHalfAwayFromZero(value: number): number {

FILE: src/renderer.ts
  type ChannelData (line 8) | type ChannelData = utils.ChannelData
  type RendererEvents (line 10) | type RendererEvents = {
  constant SMOOTH_SCROLL_FPS (line 22) | const SMOOTH_SCROLL_FPS = 60
  constant SMOOTH_SCROLL_MAX_DELTA (line 23) | const SMOOTH_SCROLL_MAX_DELTA = 10
  constant LOW_ZOOM_PIXELS_PER_SECOND_THRESHOLD (line 24) | const LOW_ZOOM_PIXELS_PER_SECOND_THRESHOLD = SMOOTH_SCROLL_MAX_DELTA * S...
  class Renderer (line 26) | class Renderer extends EventEmitter<RendererEvents> {
    method constructor (line 46) | constructor(options: WaveSurferOptions, audioElement?: HTMLElement) {
    method parentFromOptionsContainer (line 71) | private parentFromOptionsContainer(container: WaveSurferOptions['conta...
    method initEvents (line 86) | private initEvents() {
    method onContainerResize (line 127) | private onContainerResize() {
    method initDrag (line 135) | private initDrag() {
    method initHtml (line 162) | private initHtml(): [HTMLElement, ShadowRoot] {
    method setOptions (line 248) | setOptions(options: WaveSurferOptions) {
    method getWrapper (line 269) | getWrapper(): HTMLElement {
    method getWidth (line 273) | getWidth(): number {
    method getScroll (line 277) | getScroll(): number {
    method setScroll (line 281) | setScroll(pixels: number) {
    method setScrollPercentage (line 285) | setScrollPercentage(percent: number) {
    method destroy (line 291) | destroy() {
    method createDelay (line 310) | private createDelay(delayMs = 10): () => Promise<void> {
    method getHeight (line 343) | private getHeight(
    method convertColorValues (line 357) | private convertColorValues(
    method getPixelRatio (line 364) | private getPixelRatio(): number {
    method renderBarWaveform (line 368) | private renderBarWaveform(
    method renderLineWaveform (line 421) | private renderLineWaveform(
    method renderWaveform (line 445) | private renderWaveform(channelData: ChannelData, options: WaveSurferOp...
    method renderSingleCanvas (line 468) | private renderSingleCanvas(
    method renderMultiCanvas (line 512) | private renderMultiCanvas(
    method renderChannel (line 586) | private renderChannel(
    method render (line 610) | async render(audioData: AudioBuffer) {
    method reRender (line 670) | reRender() {
    method zoom (line 692) | zoom(minPxPerSec: number) {
    method scrollIntoView (line 697) | private scrollIntoView(progress: number, isPlaying = false) {
    method renderProgress (line 736) | renderProgress(progress: number, isPlaying?: boolean) {
    method exportImage (line 752) | async exportImage(format: string, quality: number, type: 'dataURL' | '...

FILE: src/state/wavesurfer-state.ts
  type WaveSurferState (line 13) | interface WaveSurferState {
  type WaveSurferActions (line 44) | interface WaveSurferActions {
  type PlayerSignals (line 63) | interface PlayerSignals {
  function createWaveSurferState (line 101) | function createWaveSurferState(playerSignals?: PlayerSignals): {

FILE: src/timer.ts
  type TimerEvents (line 3) | type TimerEvents = {
  class Timer (line 7) | class Timer extends EventEmitter<TimerEvents> {
    method start (line 11) | start() {
    method stop (line 31) | stop() {
    method destroy (line 41) | destroy() {

FILE: src/wavesurfer.ts
  type WaveSurferOptions (line 12) | type WaveSurferOptions = {
  type WaveSurferEvents (line 104) | type WaveSurferEvents = {
  class WaveSurfer (line 155) | class WaveSurfer extends Player<WaveSurferEvents> {
    method create (line 175) | public static create(options: WaveSurferOptions) {
    method getState (line 180) | public getState(): WaveSurferState {
    method getRenderer (line 185) | public getRenderer(): Renderer {
    method constructor (line 190) | constructor(options: WaveSurferOptions) {
    method updateProgress (line 249) | private updateProgress(currentTime = this.getCurrentTime()): number {
    method initTimerEvents (line 254) | private initTimerEvents() {
    method initReactiveState (line 272) | private initReactiveState() {
    method initPlayerEvents (line 281) | private initPlayerEvents() {
    method initRendererEvents (line 326) | private initRendererEvents() {
    method initPlugins (line 412) | private initPlugins() {
    method unsubscribePlayerEvents (line 420) | private unsubscribePlayerEvents() {
    method setOptions (line 426) | public setOptions(options: Partial<WaveSurferOptions>) {
    method registerPlugin (line 446) | public registerPlugin<T extends GenericPlugin>(plugin: T): T {
    method unregisterPlugin (line 466) | public unregisterPlugin(plugin: GenericPlugin): void {
    method getWrapper (line 472) | public getWrapper(): HTMLElement {
    method getWidth (line 477) | public getWidth(): number {
    method getScroll (line 482) | public getScroll(): number {
    method setScroll (line 487) | public setScroll(pixels: number) {
    method setScrollTime (line 492) | public setScrollTime(time: number) {
    method getActivePlugins (line 498) | public getActivePlugins() {
    method loadAudio (line 502) | private async loadAudio(url: string, blob?: Blob, channelData?: WaveSu...
    method load (line 569) | public async load(url: string, channelData?: WaveSurferOptions['peaks'...
    method loadBlob (line 579) | public async loadBlob(blob: Blob, channelData?: WaveSurferOptions['pea...
    method zoom (line 589) | public zoom(minPxPerSec: number) {
    method getDecodedData (line 598) | public getDecodedData(): AudioBuffer | null {
    method exportPeaks (line 603) | public exportPeaks({ channels = 2, maxLength = 8000, precision = 10_00...
    method getDuration (line 628) | public getDuration(): number {
    method toggleInteraction (line 638) | public toggleInteraction(isInteractive: boolean) {
    method setTime (line 643) | public setTime(time: number) {
    method seekTo (line 651) | public seekTo(progress: number) {
    method play (line 657) | public async play(start?: number, end?: number): Promise<void> {
    method playPause (line 675) | public async playPause(): Promise<void> {
    method stop (line 680) | public stop() {
    method skip (line 686) | public skip(seconds: number) {
    method empty (line 691) | public empty() {
    method setMediaElement (line 696) | public setMediaElement(element: HTMLMediaElement) {
    method exportImage (line 712) | public async exportImage(
    method destroy (line 721) | public destroy() {

FILE: src/webaudio.ts
  type WebAudioPlayerEvents (line 3) | type WebAudioPlayerEvents = {
  class WebAudioPlayer (line 22) | class WebAudioPlayer extends EventEmitter<WebAudioPlayerEvents> {
    method constructor (line 38) | constructor(audioContext = new AudioContext()) {
    method load (line 51) | async load() {
    method src (line 55) | get src() {
    method src (line 59) | set src(value: string) {
    method _play (line 96) | private _play() {
    method _pause (line 130) | private _pause() {
    method play (line 136) | async play() {
    method pause (line 142) | pause() {
    method stopAt (line 148) | stopAt(timeSeconds: number) {
    method setSinkId (line 165) | async setSinkId(deviceId: string) {
    method playbackRate (line 170) | get playbackRate() {
    method playbackRate (line 173) | set playbackRate(value) {
    method currentTime (line 184) | get currentTime() {
    method currentTime (line 189) | set currentTime(value) {
    method duration (line 200) | get duration() {
    method duration (line 203) | set duration(value: number) {
    method volume (line 207) | get volume() {
    method volume (line 210) | set volume(value) {
    method muted (line 215) | get muted() {
    method muted (line 218) | set muted(value: boolean) {
    method canPlayType (line 229) | public canPlayType(mimeType: string) {
    method getGainNode (line 234) | public getGainNode(): GainNode {
    method getChannelData (line 239) | public getChannelData(): Float32Array[] {
    method removeAttribute (line 252) | public removeAttribute(attrName: string) {
Condensed preview — 138 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (685K chars).
[
  {
    "path": ".editorconfig",
    "chars": 174,
    "preview": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 20,
    "preview": "github: [katspaugh]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 723,
    "preview": "---\nname: Bug report\nabout: Report a bug you found in wavesurfer.js\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n<!--\nBEFOR"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "chars": 367,
    "preview": "---\nname: Question\nabout: Have a question or facing a roadblock with wavesurfer?\ntitle: 'DO NOT CREATE THIS ISSUE – 不要创建"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 193,
    "preview": "## Short description\nResolves #\n\n## Implementation details\n\n\n## How to test it\n\n\n## Screenshots\n\n\n## Checklist\n* [ ] Thi"
  },
  {
    "path": ".github/workflows/build/action.yml",
    "chars": 139,
    "preview": "name: 'Build'\n\ndescription: 'Build the app'\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Build\n      shell: bash\n   "
  },
  {
    "path": ".github/workflows/e2e.yml",
    "chars": 650,
    "preview": "name: e2e\n\non:\n  pull_request:\n\njobs:\n  e2e:\n    runs-on: ubuntu-latest\n    name: E2E tests\n    steps:\n      - uses: act"
  },
  {
    "path": ".github/workflows/label-sponsors.yml",
    "chars": 287,
    "preview": "name: Label sponsors\non:\n  pull_request:\n    types: [opened]\n  issues:\n    types: [opened]\njobs:\n  build:\n    name: is-s"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 458,
    "preview": "name: Lint\non: [pull_request]\n\njobs:\n  eslint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2113,
    "preview": "name: Release\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: write\n\njobs:\n  publish-npm:\n    runs-on:"
  },
  {
    "path": ".github/workflows/unit-tests.yml",
    "chars": 219,
    "preview": "name: Unit Tests\non: [pull_request]\n\njobs:\n  jest:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@"
  },
  {
    "path": ".github/workflows/yarn/action.yml",
    "chars": 398,
    "preview": "name: 'Yarn'\n\ndescription: 'Install node modules'\n\nruns:\n  using: 'composite'\n  steps:\n    - uses: actions/setup-node@v3"
  },
  {
    "path": ".gitignore",
    "chars": 163,
    "preview": ".DS_Store\ndist\ndocs\nnode_modules\nyarn-error.log\ncypress/screenshots\ncypress/downloads\ncypress/videos\ncypress/**/__diff_o"
  },
  {
    "path": ".prettierrc",
    "chars": 130,
    "preview": "{\n  \"tabWidth\": 2,\n  \"printWidth\": 120,\n  \"trailingComma\": \"all\",\n  \"singleQuote\": true,\n  \"semi\": false,\n  \"endOfLine\":"
  },
  {
    "path": "AGENTS.md",
    "chars": 713,
    "preview": "# Coding Conventions\n\n- Use TypeScript. Prefer ES modules.\n- Follow the repo Prettier configuration (2 spaces, print wid"
  },
  {
    "path": "AI_OVERVIEW.md",
    "chars": 1594,
    "preview": "# Repository Overview for AI Agents\n\nThis document gives a condensed view of the project structure and build process so "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1652,
    "preview": "# CONTRIBUTING to wavesurfer.js\n\nHello there,\n\nFirstly, a heartfelt thank you! We sincerely appreciate your interest in "
  },
  {
    "path": "LICENSE",
    "chars": 1531,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2012-2023, katspaugh and contributors\nAll rights reserved.\n\nRedistribution and use i"
  },
  {
    "path": "README.md",
    "chars": 7472,
    "preview": "# <img src=\"https://user-images.githubusercontent.com/381895/226091100-f5567a28-7736-4d37-8f84-e08f297b7e1a.png\" alt=\"lo"
  },
  {
    "path": "cypress/e2e/abort.cy.js",
    "chars": 2084,
    "preview": "describe('WaveSurfer abort handling tests', () => {\n  beforeEach(() => {\n    cy.visit('cypress/e2e/index.html')\n\n    cy."
  },
  {
    "path": "cypress/e2e/basic.cy.js",
    "chars": 9957,
    "preview": "describe('WaveSurfer basic tests', () => {\n  beforeEach((done) => {\n    cy.visit('cypress/e2e/index.html')\n\n    cy.windo"
  },
  {
    "path": "cypress/e2e/envelope.cy.js",
    "chars": 2068,
    "preview": "const id = '#waveform'\n\ndescribe('WaveSurfer Envelope plugin tests', () => {\n  it('should render an envelope', () => {\n "
  },
  {
    "path": "cypress/e2e/error.cy.js",
    "chars": 694,
    "preview": "describe('WaveSurfer error handling tests', () => {\n  it('should fire error event if provided file url does not exist', "
  },
  {
    "path": "cypress/e2e/hover.cy.js",
    "chars": 3580,
    "preview": "const id = '#waveform'\n\ndescribe('WaveSurfer Hover plugin tests', () => {\n  it('should render a label to the right with "
  },
  {
    "path": "cypress/e2e/index.html",
    "chars": 924,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "cypress/e2e/options.cy.js",
    "chars": 18389,
    "preview": "const id = '#waveform'\nconst otherId = '#otherWaveform'\n\nconst wrapReady = (wavesurfer, event = 'ready') => {\n  const wa"
  },
  {
    "path": "cypress/e2e/regions-no-audio.cy.js",
    "chars": 3356,
    "preview": "describe('WaveSurfer Regions plugin with no audio tests', () => {\n  beforeEach((done) => {\n    cy.visit('cypress/e2e/ind"
  },
  {
    "path": "cypress/e2e/regions.cy.js",
    "chars": 9908,
    "preview": "describe('WaveSurfer Regions plugin tests', () => {\n  beforeEach((done) => {\n    cy.visit('cypress/e2e/index.html')\n\n   "
  },
  {
    "path": "cypress/e2e/spectrogram.cy.js",
    "chars": 3299,
    "preview": "const id = '#waveform'\nconst scales = ['linear', 'mel', 'log', 'bark', 'erb']\n\nxdescribe('WaveSurfer Spectrogram plugin "
  },
  {
    "path": "cypress/e2e/umd.cy.js",
    "chars": 596,
    "preview": "describe('WaveSurfer UMD module tests', () => {\n  beforeEach(() => {\n    cy.visit('cypress/e2e/umd.html')\n    cy.window("
  },
  {
    "path": "cypress/e2e/umd.html",
    "chars": 532,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "cypress/e2e/webaudio.cy.js",
    "chars": 2825,
    "preview": "import WebAudioPlayer from '../../dist/webaudio.js'\n\ndescribe('WebAudioPlayer', () => {\n  beforeEach(() => {\n    cy.wind"
  },
  {
    "path": "cypress/support/commands.ts",
    "chars": 1490,
    "preview": "/// <reference types=\"cypress\" />\n// ***********************************************\n// This example commands.ts shows y"
  },
  {
    "path": "cypress/support/e2e.ts",
    "chars": 668,
    "preview": "// ***********************************************************\n// This example support/e2e.ts is processed and\n// loaded"
  },
  {
    "path": "cypress.config.js",
    "chars": 521,
    "preview": "import { defineConfig } from 'cypress'\nimport { addMatchImageSnapshotPlugin } from 'cypress-image-snapshot/plugin.js'\n\ne"
  },
  {
    "path": "eslint.config.js",
    "chars": 1016,
    "preview": "import { FlatCompat } from '@eslint/eslintrc'\nimport js from '@eslint/js'\n\nconst compat = new FlatCompat({\n  baseDirecto"
  },
  {
    "path": "examples/_preview.js",
    "chars": 2081,
    "preview": "const iframe = document.querySelector('iframe')\nconst textarea = document.querySelector('textarea')\n\nconst loadPreview ="
  },
  {
    "path": "examples/all-options.js",
    "chars": 4745,
    "preview": "// All wavesurfer options in one place\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst options = {\n  /** HTML element or "
  },
  {
    "path": "examples/audio/reed.sh",
    "chars": 80,
    "preview": "#!/bin/bash\n\nsay -v 'Reed (English (US))' -o \"${1}\" --file-format='mp4f' \"${1}\"\n"
  },
  {
    "path": "examples/bars.js",
    "chars": 445,
    "preview": "// SoundCloud-style bars\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container: do"
  },
  {
    "path": "examples/basic.js",
    "chars": 290,
    "preview": "// A basic example\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container: document"
  },
  {
    "path": "examples/custom-render.js",
    "chars": 1229,
    "preview": "// Custom rendering function\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container"
  },
  {
    "path": "examples/envelope.js",
    "chars": 2287,
    "preview": "// Envelope plugin\n// Graphical fade-in and fade-out and volume control\n\n/*\n<html>\n  <button style=\"min-width: 5em\" id=\""
  },
  {
    "path": "examples/events.js",
    "chars": 2926,
    "preview": "import WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container: document.body,\n  waveColor: "
  },
  {
    "path": "examples/fm-synth.js",
    "chars": 7368,
    "preview": "// A two-operator FM synth with a real-time waveform\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSur"
  },
  {
    "path": "examples/gradient.js",
    "chars": 716,
    "preview": "// Fancy gradients\n\nimport WaveSurfer from 'wavesurfer.js'\n\n// Create a canvas gradient\nconst ctx = document.createEleme"
  },
  {
    "path": "examples/hover.js",
    "chars": 826,
    "preview": "// Hover plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport Hover from 'wavesurfer.js/dist/plugins/hover.esm.js'\n\n// "
  },
  {
    "path": "examples/minimap.js",
    "chars": 822,
    "preview": "// Minimap plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport Minimap from 'wavesurfer.js/dist/plugins/minimap.esm.js"
  },
  {
    "path": "examples/multitrack.js",
    "chars": 5351,
    "preview": "/**\n * Multi-track mixer\n *\n * @see https://github.com/katspaugh/wavesurfer-multitrack\n */\n\n/*\n<html>\n  <script src=\"htt"
  },
  {
    "path": "examples/phase-vocoder/index.js",
    "chars": 1810,
    "preview": "// WebAudio speed control with pitch preservation\n\nimport WaveSurfer from 'wavesurfer.js'\n\n// Init wavesurfer\nconst wave"
  },
  {
    "path": "examples/pitch-worker.js",
    "chars": 973,
    "preview": "import Pitchfinder from 'https://esm.sh/pitchfinder'\n\nonmessage = (e) => {\n  const { peaks, sampleRate = 8000, algo = 'A"
  },
  {
    "path": "examples/pitch.js",
    "chars": 2973,
    "preview": "import WaveSurfer from 'wavesurfer.js'\n\nconst pitchWorker = new Worker('/examples/pitch-worker.js', { type: 'module' })\n"
  },
  {
    "path": "examples/predecoded.js",
    "chars": 1943,
    "preview": "// With pre-decoded audio data\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  contain"
  },
  {
    "path": "examples/react-global-player.js",
    "chars": 3651,
    "preview": "// React example\n\n/*\n  <html>\n    <script src=\"https://unpkg.com/react@18/umd/react.production.min.js\"></script>\n    <sc"
  },
  {
    "path": "examples/react.js",
    "chars": 2307,
    "preview": "// React example\n// See https://github.com/katspaugh/wavesurfer-react\n\nimport * as React from 'react'\nconst { useMemo, u"
  },
  {
    "path": "examples/record-sync.js",
    "chars": 5561,
    "preview": "// Record plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm"
  },
  {
    "path": "examples/record.js",
    "chars": 4877,
    "preview": "// Record plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm"
  },
  {
    "path": "examples/regions.js",
    "chars": 4069,
    "preview": "// Regions plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport RegionsPlugin from 'wavesurfer.js/dist/plugins/regions."
  },
  {
    "path": "examples/silence.js",
    "chars": 2729,
    "preview": "// Silence detection example\n\nimport WaveSurfer from 'wavesurfer.js'\nimport RegionsPlugin from 'wavesurfer.js/dist/plugi"
  },
  {
    "path": "examples/soundcloud.js",
    "chars": 3304,
    "preview": "// Soundcloud-style player\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst canvas = document.createElement('canvas')\ncons"
  },
  {
    "path": "examples/spectrogram-windowed.js",
    "chars": 2355,
    "preview": "// Windowed Spectrogram plugin - Optimized for very long audio files\n\nimport WaveSurfer from 'wavesurfer.js'\nimport Wind"
  },
  {
    "path": "examples/spectrogram.js",
    "chars": 5326,
    "preview": "// Spectrogram plugin example\n\nimport WaveSurfer from 'wavesurfer.js'\nimport Spectrogram from 'wavesurfer.js/dist/plugin"
  },
  {
    "path": "examples/speed.js",
    "chars": 1382,
    "preview": "// Set the playback speed\n\n/*\n<html>\n  <div style=\"display: flex; margin: 1rem 0; gap: 1rem;\">\n    <button>\n      Play/p"
  },
  {
    "path": "examples/split-channels.js",
    "chars": 433,
    "preview": "// Split channels\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container: document."
  },
  {
    "path": "examples/styling.js",
    "chars": 2316,
    "preview": "// Custom styling via CSS\n\n/*\n  <html>\n    <style>\n      #waveform ::part(wrapper) {\n        --box-size: 10px;\n        b"
  },
  {
    "path": "examples/timeline-custom.js",
    "chars": 1142,
    "preview": "// Customized Timeline plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport TimelinePlugin from 'wavesurfer.js/dist/plu"
  },
  {
    "path": "examples/timeline.js",
    "chars": 1117,
    "preview": "// Timeline plugin\n\nimport WaveSurfer from 'wavesurfer.js'\nimport TimelinePlugin from 'wavesurfer.js/dist/plugins/timeli"
  },
  {
    "path": "examples/video.js",
    "chars": 520,
    "preview": "// Waveform for a video\n\n// Create a video element\n/*\n<html>\n  <video\n    src=\"/examples/audio/modular.mp4\"\n    controls"
  },
  {
    "path": "examples/vowels.js",
    "chars": 1562,
    "preview": "// American English vowels\n\nimport WaveSurfer from 'wavesurfer.js'\nimport Spectrogram from 'wavesurfer.js/dist/plugins/s"
  },
  {
    "path": "examples/webaudio-shim.js",
    "chars": 502,
    "preview": "import WaveSurfer from 'wavesurfer.js'\nimport WebAudioPlayer from 'wavesurfer.js/dist/webaudio.js'\n\nconst webAudioPlayer"
  },
  {
    "path": "examples/webaudio.js",
    "chars": 2008,
    "preview": "// Web Audio example\n\nimport WaveSurfer from 'wavesurfer.js'\n\n// Define the equalizer bands\nconst eqBands = [32, 64, 125"
  },
  {
    "path": "examples/zoom-plugin.js",
    "chars": 1702,
    "preview": "/**\n * Zoom plugin\n *\n * Zoom in or out on the waveform when scrolling the mouse wheel\n */\n\nimport WaveSurfer from 'wave"
  },
  {
    "path": "examples/zoom.js",
    "chars": 1740,
    "preview": "// Zooming the waveform\n\nimport WaveSurfer from 'wavesurfer.js'\n\nconst wavesurfer = WaveSurfer.create({\n  container: doc"
  },
  {
    "path": "index.html",
    "chars": 4900,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "jest.config.js",
    "chars": 342,
    "preview": "export default {\n  preset: 'ts-jest/presets/default-esm',\n  testEnvironment: 'jsdom',\n  roots: ['<rootDir>/src'],\n  modu"
  },
  {
    "path": "package.json",
    "chars": 2901,
    "preview": "{\n  \"name\": \"wavesurfer.js\",\n  \"version\": \"7.12.4\",\n  \"license\": \"BSD-3-Clause\",\n  \"author\": \"katspaugh\",\n  \"description"
  },
  {
    "path": "rollup.config.js",
    "chars": 2583,
    "preview": "import { glob } from 'glob'\nimport typescript from '@rollup/plugin-typescript'\nimport terser from '@rollup/plugin-terser"
  },
  {
    "path": "scripts/clean.cjs",
    "chars": 190,
    "preview": "const path = require('path')\nconst fs = require('fs')\n\nconst run = () => {\n  const distPath = path.join(__dirname, '../d"
  },
  {
    "path": "scripts/plugin.sh",
    "chars": 350,
    "preview": "#!/bin/bash\n\n# Plugin name from argument\nPLUGIN_NAME=$1\n\n# Prompt for plugin name if not provided\nif [ -z \"$PLUGIN_NAME\""
  },
  {
    "path": "scripts/plugin.ts.template",
    "chars": 886,
    "preview": "/**\n * The Template plugin\n */\n\nimport BasePlugin, { type BasePluginEvents } from '../base-plugin.js'\n\nexport type Templ"
  },
  {
    "path": "src/__tests__/base-plugin.test.ts",
    "chars": 777,
    "preview": "import { BasePlugin } from '../base-plugin.js'\n\nclass TestPlugin extends BasePlugin<{ destroy: [] }, {}> {\n  initCalled "
  },
  {
    "path": "src/__tests__/dom.test.ts",
    "chars": 504,
    "preview": "import createElement from '../dom.js'\n\ndescribe('createElement', () => {\n  test('creates DOM structure', () => {\n    con"
  },
  {
    "path": "src/__tests__/draggable.test.ts",
    "chars": 1580,
    "preview": "import { makeDraggable } from '../draggable.js'\n\ndescribe('makeDraggable', () => {\n  beforeAll(() => {\n    Object.define"
  },
  {
    "path": "src/__tests__/event-emitter.test.ts",
    "chars": 875,
    "preview": "import EventEmitter from '../event-emitter.js'\n\ninterface Events {\n  foo: [number]\n  bar: []\n  [key: string]: unknown[]\n"
  },
  {
    "path": "src/__tests__/fetcher.test.ts",
    "chars": 1073,
    "preview": "import Fetcher from '../fetcher.js'\nimport { TextEncoder } from 'util'\nimport { Blob as NodeBlob } from 'buffer'\n\ndescri"
  },
  {
    "path": "src/__tests__/memory-leaks.test.ts",
    "chars": 10517,
    "preview": "/**\n * Memory Leak Detection Tests\n *\n * These tests verify that WaveSurfer properly cleans up resources\n * and doesn't "
  },
  {
    "path": "src/__tests__/minimap.test.ts",
    "chars": 3657,
    "preview": "jest.mock('../wavesurfer.js', () => ({\n  __esModule: true,\n  default: {\n    create: jest.fn(),\n  },\n}))\n\nimport MinimapP"
  },
  {
    "path": "src/__tests__/player.test.ts",
    "chars": 6974,
    "preview": "import Player from '../player.js'\n\ninterface Events {\n  [key: string]: unknown[]\n}\n\ndescribe('Player', () => {\n  const c"
  },
  {
    "path": "src/__tests__/regions.test.ts",
    "chars": 2437,
    "preview": "import RegionsPlugin from '../plugins/regions.js'\n\ntype Listener = (...args: any[]) => void\n\nconst createEmitter = () =>"
  },
  {
    "path": "src/__tests__/renderer-utils.test.ts",
    "chars": 13685,
    "preview": "import {\n  MAX_CANVAS_WIDTH,\n  MAX_NODES,\n  calculateBarHeights,\n  calculateBarRenderConfig,\n  calculateBarSegments,\n  c"
  },
  {
    "path": "src/__tests__/renderer.test.ts",
    "chars": 8872,
    "preview": "import Renderer from '../renderer.js'\n\ndeclare global {\n  interface Window {\n    HTMLCanvasElement: typeof HTMLCanvasEle"
  },
  {
    "path": "src/__tests__/timeline.test.ts",
    "chars": 1920,
    "preview": "import TimelinePlugin from '../plugins/timeline.js'\nimport { signal } from '../reactive/store.js'\n\ntype Listener = (...a"
  },
  {
    "path": "src/__tests__/timer.test.ts",
    "chars": 460,
    "preview": "import Timer from '../timer.js'\n\ndescribe('Timer', () => {\n  test('start schedules ticks', () => {\n    const timer = new"
  },
  {
    "path": "src/__tests__/wavesurfer.test.ts",
    "chars": 7657,
    "preview": "jest.mock('../renderer.js', () => {\n  let lastInstance: any\n  class Renderer {\n    options: any\n    wrapper = document.c"
  },
  {
    "path": "src/base-plugin.ts",
    "chars": 1307,
    "preview": "import EventEmitter from './event-emitter.js'\nimport type WaveSurfer from './wavesurfer.js'\n\nexport type BasePluginEvent"
  },
  {
    "path": "src/decoder.ts",
    "chars": 2409,
    "preview": "/** Decode an array buffer into an audio buffer */\nasync function decode(audioData: ArrayBuffer, sampleRate: number): Pr"
  },
  {
    "path": "src/dom.ts",
    "chars": 1668,
    "preview": "type TreeNode = { [key: string]: string | number | boolean | CSSStyleDeclaration | TreeNode | Node } & {\n  xmlns?: strin"
  },
  {
    "path": "src/draggable.ts",
    "chars": 3743,
    "preview": "/**\n * @deprecated Use createDragStream from './reactive/drag-stream.js' instead.\n * This function is maintained for bac"
  },
  {
    "path": "src/event-emitter.ts",
    "chars": 2233,
    "preview": "export type GeneralEventTypes = {\n  // the name of the event and the data it dispatches with\n  // e.g. 'entryCreated': ["
  },
  {
    "path": "src/fetcher.ts",
    "chars": 1433,
    "preview": "async function watchProgress(response: Response, progressCallback: (percentage: number) => void) {\n  if (!response.body "
  },
  {
    "path": "src/fft.ts",
    "chars": 22440,
    "preview": "/**\n * FFT (Fast Fourier Transform) implementation\n * Based on https://github.com/corbanbrook/dsp.js\n *\n * Centralized F"
  },
  {
    "path": "src/player.ts",
    "chars": 8789,
    "preview": "import EventEmitter, { type GeneralEventTypes } from './event-emitter.js'\nimport { signal, type WritableSignal } from '."
  },
  {
    "path": "src/plugins/envelope.ts",
    "chars": 16218,
    "preview": "/**\n * Envelope is a visual UI for controlling the audio volume and add fade-in and fade-out effects.\n */\n\nimport BasePl"
  },
  {
    "path": "src/plugins/hover.ts",
    "chars": 6547,
    "preview": "/**\n * The Hover plugin follows the mouse and shows a timestamp\n */\n\nimport BasePlugin, { type BasePluginEvents } from '"
  },
  {
    "path": "src/plugins/minimap.ts",
    "chars": 9646,
    "preview": "/**\n * Minimap is a tiny copy of the main waveform serving as a navigation tool.\n */\n\nimport BasePlugin, { type BasePlug"
  },
  {
    "path": "src/plugins/record.ts",
    "chars": 13163,
    "preview": "/**\n * Record audio from the microphone with a real-time waveform preview\n */\n\nimport BasePlugin, { type BasePluginEvent"
  },
  {
    "path": "src/plugins/regions.ts",
    "chars": 28153,
    "preview": "/**\n * Regions are visual overlays on the waveform that can be used to mark segments of audio.\n * Regions can be clicked"
  },
  {
    "path": "src/plugins/spectrogram-windowed.ts",
    "chars": 41069,
    "preview": "/**\n * Windowed Spectrogram plugin - Optimized for very long audio files\n *\n * Only renders frequency data in a sliding "
  },
  {
    "path": "src/plugins/spectrogram-worker.ts",
    "chars": 3846,
    "preview": "/**\n * Web Worker for Windowed Spectrogram Plugin\n * Handles FFT calculations for frequency analysis\n */\n\n// Import cent"
  },
  {
    "path": "src/plugins/spectrogram.ts",
    "chars": 36119,
    "preview": "/**\n * Spectrogram plugin\n *\n * Render a spectrogram visualisation of the audio.\n *\n * @author Pavel Denisov (https://gi"
  },
  {
    "path": "src/plugins/timeline.ts",
    "chars": 10072,
    "preview": "/**\n * The Timeline plugin adds timestamps and notches under the waveform.\n */\n\nimport BasePlugin, { type BasePluginEven"
  },
  {
    "path": "src/plugins/zoom.ts",
    "chars": 9873,
    "preview": "/**\n * Zoom plugin\n *\n * Zoom in or out on the waveform when scrolling the mouse wheel\n *\n * @author HoodyHuo (https://g"
  },
  {
    "path": "src/reactive/README.md",
    "chars": 5520,
    "preview": "# Reactive System\n\nSignal-based reactivity for WaveSurfer.js, providing automatic state management and efficient updates"
  },
  {
    "path": "src/reactive/__tests__/drag-stream.test.ts",
    "chars": 7723,
    "preview": "import { createDragStream, type DragEvent } from '../drag-stream'\n\ndescribe('createDragStream', () => {\n  beforeAll(() ="
  },
  {
    "path": "src/reactive/__tests__/event-stream-emitter.test.ts",
    "chars": 9617,
    "preview": "import { toStream, toStreams, mergeStreams, mapStream, filterStream } from '../event-stream-emitter'\nimport EventEmitter"
  },
  {
    "path": "src/reactive/__tests__/event-streams.test.ts",
    "chars": 9525,
    "preview": "import { signal } from '../store'\nimport { fromEvent, map, filter, debounce, throttle, cleanup } from '../event-streams'"
  },
  {
    "path": "src/reactive/__tests__/media-event-bridge.test.ts",
    "chars": 7623,
    "preview": "import { bridgeMediaEvents, bridgeMediaEventsWithHandler } from '../media-event-bridge'\nimport { createWaveSurferState }"
  },
  {
    "path": "src/reactive/__tests__/render-scheduler.test.ts",
    "chars": 8112,
    "preview": "import { RenderScheduler } from '../render-scheduler'\n\ndescribe('RenderScheduler', () => {\n  let scheduler: RenderSchedu"
  },
  {
    "path": "src/reactive/__tests__/scroll-stream.test.ts",
    "chars": 6661,
    "preview": "import {\n  createScrollStream,\n  createScrollStreamWithAction,\n  calculateScrollPercentages,\n  calculateScrollBounds,\n  "
  },
  {
    "path": "src/reactive/__tests__/state-event-emitter.test.ts",
    "chars": 9613,
    "preview": "import {\n  setupStateEventEmission,\n  setupSignalEventEmission,\n  setupDebouncedEventEmission,\n  setupConditionalEventEm"
  },
  {
    "path": "src/reactive/__tests__/store.test.ts",
    "chars": 8935,
    "preview": "import { signal, computed, effect } from '../store'\n\ndescribe('signal', () => {\n  it('should create a signal with initia"
  },
  {
    "path": "src/reactive/drag-stream.ts",
    "chars": 5178,
    "preview": "/**\n * Reactive drag stream utilities\n *\n * Provides declarative drag handling using reactive streams.\n * Automatically "
  },
  {
    "path": "src/reactive/event-stream-emitter.ts",
    "chars": 5168,
    "preview": "/**\n * Event stream emitter - bridges EventEmitter to reactive streams\n *\n * Provides reactive stream API on top of trad"
  },
  {
    "path": "src/reactive/event-streams.ts",
    "chars": 4361,
    "preview": "/**\n * Event stream utilities for converting DOM events to reactive signals\n *\n * These utilities allow composing event "
  },
  {
    "path": "src/reactive/media-event-bridge.ts",
    "chars": 5186,
    "preview": "/**\n * Media event bridge utilities\n *\n * Bridges HTMLMediaElement events to reactive state updates.\n * Provides a clean"
  },
  {
    "path": "src/reactive/render-scheduler.ts",
    "chars": 2204,
    "preview": "/**\n * RenderScheduler batches multiple render requests into a single frame using requestAnimationFrame.\n * This prevent"
  },
  {
    "path": "src/reactive/scroll-stream.ts",
    "chars": 5032,
    "preview": "/**\n * Reactive scroll stream utilities\n *\n * Provides declarative scroll handling using reactive streams.\n * Automatica"
  },
  {
    "path": "src/reactive/state-event-emitter.ts",
    "chars": 7838,
    "preview": "/**\n * State-driven event emission utilities\n *\n * Automatically emit events when reactive state changes.\n * Ensures eve"
  },
  {
    "path": "src/reactive/store.ts",
    "chars": 3921,
    "preview": "/**\n * Reactive primitives for managing state in WaveSurfer\n *\n * This module provides signal-based reactivity similar t"
  },
  {
    "path": "src/renderer-utils.ts",
    "chars": 10790,
    "preview": "import type { WaveSurferOptions } from './wavesurfer.js'\n\nexport type ChannelData = Array<Float32Array | number[]>\n\nexpo"
  },
  {
    "path": "src/renderer.ts",
    "chars": 24540,
    "preview": "import EventEmitter from './event-emitter.js'\nimport * as utils from './renderer-utils.js'\nimport type { WaveSurferOptio"
  },
  {
    "path": "src/state/__tests__/wavesurfer-state.test.ts",
    "chars": 10590,
    "preview": "import { createWaveSurferState } from '../wavesurfer-state'\n\ndescribe('WaveSurferState', () => {\n  it('should create sta"
  },
  {
    "path": "src/state/wavesurfer-state.ts",
    "chars": 5761,
    "preview": "/**\n * Centralized reactive state for WaveSurfer\n *\n * This module provides a single source of truth for all WaveSurfer "
  },
  {
    "path": "src/timer.ts",
    "chars": 865,
    "preview": "import EventEmitter from './event-emitter.js'\n\ntype TimerEvents = {\n  tick: []\n}\n\nclass Timer extends EventEmitter<Timer"
  },
  {
    "path": "src/wavesurfer.ts",
    "chars": 24090,
    "preview": "import BasePlugin, { type GenericPlugin } from './base-plugin.js'\nimport Decoder from './decoder.js'\nimport * as dom fro"
  },
  {
    "path": "src/webaudio.ts",
    "chars": 6764,
    "preview": "import EventEmitter from './event-emitter.js'\n\ntype WebAudioPlayerEvents = {\n  loadedmetadata: []\n  canplay: []\n  play: "
  },
  {
    "path": "tsconfig.json",
    "chars": 370,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"lib\": [\n      \"ESNext\",\n      \"DOM\"\n    ],\n    \"allowUmdGlobalAccess\""
  },
  {
    "path": "tsconfig.test.json",
    "chars": 117,
    "preview": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"jest\", \"node\"],\n    \"module\": \"ESNext\"\n  }\n}\n"
  }
]

About this extraction

This page contains the full source code of the katspaugh/wavesurfer.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 138 files (639.0 KB), approximately 171.4k tokens, and a symbol index with 505 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!