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: '' --- ## 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 ) - **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 ================================================ # logo 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. waveform screenshot **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 ``` 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 ``` 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
I'm having CORS issues 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 the same domain or another domain if and only if that domain enables CORS. 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).
Does wavesurfer support large files? 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 this example). You can use a tool like audiowaveform to generate peaks.
What about streaming audio? Streaming audio is supported only with pre-decoded peaks and duration.
There is a mismatch between my audio and the waveform. How do I fix it? 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).

Alternatively, you can use the Web Audio shim which is more accurate.

How do I connect wavesurfer.js to Web Audio effects? 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 this example) but nothign more than that. Please don't expect wavesurfer to be able to cut, add effects, or process your audio in any way.
## 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 ================================================ WaveSurfer Test
================================================ 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 = '

HTML content

' 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 ================================================ WaveSurfer CommonJS Test
================================================ 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 ================================================ /// // *********************************************** // 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 // drag(subject: string, options?: Partial): Chainable // dismiss(subject: string, options?: Partial): Chainable // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable // } // } // } 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>/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 = ` wavesurfer.js examples ${html.join('')} ` // 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 /* Volume:

📖 Envelope plugin docs

*/ 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') /*

Open the console to see the event logs

*/ // 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() /*

Hold Shift to play the notes one octave higher

*/ ================================================ 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() }) /*

📖 Hover plugin docs

*/ ================================================ 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() }) /*

📖 Minimap plugin docs

*/ ================================================ FILE: examples/multitrack.js ================================================ /** * Multi-track mixer * * @see https://github.com/katspaugh/wavesurfer-multitrack */ /*
*/ // 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() }) }) /*

📖 Based on github.com/olvb/phaze

*/ ================================================ 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() } } /*

Audio from LibriVox

Drag-n-drop your own audio file
*/ ================================================ 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 /* */ // 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 (
) }) const Playlist = memo(({ urls, setCurrentPlayer }) => { return urls.map((url, index) => ( )) }) const audioUrls = ['/examples/audio/audio.wav', '/examples/audio/demo.wav', '/examples/audio/stereo.mp3'] const App = () => { const [currentPlayer, setCurrentPlayer] = useState() return ( <>

Playlist

Global player

{currentPlayer && ( )} ) } // Create a React root and render the app const root = ReactDOM.createRoot(document.body) root.render() ================================================ 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 ( <>

Current audio: {audioUrls[urlIndex]}

Current time: {formatTime(currentTime)}

) } // Create a React root and render the app const root = createRoot(document.body) root.render() /* */ ================================================ 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() }) /*

Press Record to start recording 🎙️

📖 Record plugin docs

00:00

*/ ================================================ 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() /*

Press Record to start recording 🎙️

📖 Record plugin docs

00:00

*/ ================================================ 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) } }) /*

Regions plugin docs

*/ ================================================ 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))) } /*
0:00
0:00
*/ ================================================ 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() }) /*
Zoom level: 50 px/s

📖 Windowed Spectrogram plugin docs

⚡ This plugin is optimized for very long audio files by using a sliding window approach that keeps memory usage constant regardless of audio length.

🔍 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!

*/ ================================================ 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()) }) /*
⚠️ Important Note: For audio files that require scrolling (longer than the container width), you MUST set a minPxPerSec value in the WaveSurfer configuration to ensure proper spectrogram rendering. Without this, the spectrogram may not display correctly.

Spectrogram Settings

Visual Options

  • labels: true/false - Show frequency labels on the left
  • height: 200 - Spectrogram height in pixels
  • splitChannels: true/false - Separate spectrograms for each audio channel
  • labelsBackground: 'rgba(0,0,0,0.1)' - Background color for labels
  • labelsColor: '#fff' - Text color for frequency labels

Frequency Settings

  • scale: 'mel'|'linear'|'logarithmic'|'bark'|'erb' - Frequency scale type
  • frequencyMax: 8000 - Maximum frequency to display (Hz)
  • frequencyMin: 0 - Minimum frequency to display (Hz)

Performance Settings

  • fftSamples: 1024 - FFT resolution (512, 1024, 2048, 4096)
  • useWebWorker: true - Use web worker for faster processing
  • maxCanvasWidth: 30000 - Split large spectrograms into multiple canvases
  • noverlap: null - Overlap between FFT windows (auto-calculated)

Color & Styling

  • colorMap: 'gray'|'igray'|'roseus' - Color scheme for frequency intensity
  • gainDB: 20 - Brightness adjustment (-20 to +40)
  • rangeDB: 80 - Dynamic range (20 to 120)
  • windowFunc: 'hann' - FFT window function (hann, hamming, blackman, etc.)

📖 Full Documentation

*/ ================================================ FILE: examples/speed.js ================================================ // Set the playback speed /*
*/ 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 /*
*/ 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() }) /*

📖 Timeline plugin docs

*/ ================================================ 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) }) /*

📖 Timeline plugin docs

*/ // 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 /*