Skip to main content

Interactive audio waveform players for Jupyter notebooks, powered by wavesurfer.js

Project description

wavesurf

A complete Python wrapper of wavesurfer.js.

wavesurfer.js is an open-source audio visualization library for building interactive, customizable waveform players. wavesurf brings its full power to Python -- pass a numpy array, torch tensor, file path, or URL and get a fully-featured audio player inline, complete with themes, plugins, custom controls, and event handlers. No JavaScript required.

This implementation also includes widgets for use in Jupyter notebooks, and a detailed developer guide for implementing your own sexy Jupyter widgets using the library!

Gallery

Basic Player Bar-Style Waveforms
Basic Player Bar-Style Waveforms
Gradient Colors Timeline Plugin
Gradient Colors Timeline Plugin
Player Controls Custom Theme
Player Controls Custom Theme
Grid Layout Built-in Themes
Grid Layout Built-in Themes

Examples

Interactive HTML examples demonstrating all major features are available here (open in browser for full interactivity):

Example Description
Basic Player Simplest player with default DARK theme
Bar-Style Waveforms Customize bar width, gap, and radius
Gradient Colors Gradient wave and progress colors
Timeline Plugin Time markers below the waveform
Minimap Plugin Miniature waveform overview
Spectrogram Plugin Frequency spectrum visualization
Regions Plugin Highlight and annotate audio segments
Player Controls Play, time, volume, and rate controls
Grid Layout Multi-player comparison grids
Custom Theme Fully customized visual theme
Built-in Themes DARK and LIGHT themes side by side

Regenerate examples locally:

uv run python examples/generate_examples.py

Features

  • Two-line quick start -- display_audio(audio=array, sr=24000) renders a complete player
  • Four audio input types -- numpy arrays, PyTorch tensors, file paths, and URLs
  • Two built-in themes -- DARK and LIGHT, plus a theme registry for custom branded themes
  • Fully customizable themes -- 25+ styling properties for waveform, container, buttons, and decorative elements
  • Flexible controls -- play/pause, time display, volume slider, playback rate selector
  • 22 event handlers -- attach raw JavaScript callbacks to any wavesurfer.js event
  • Plugin support -- timeline, minimap, regions, and spectrogram plugins
  • Comparison layouts -- side-by-side and grid layouts for multi-player displays
  • Immutable builder pattern -- chain .with_theme(), .with_options(), .with_events() fluently
  • JupyterLab compatible -- iframe-based rendering bypasses JupyterLab's script stripping

Installation

uv add wavesurf

Quick Start

from wavesurf import display_audio
import numpy as np

# Generate a 1-second sine wave
t = np.linspace(start=0, stop=1, num=24000, dtype=np.float32)
audio = np.sin(2 * np.pi * 440 * t)

display_audio(audio=audio, sr=24000, title="440 Hz Sine Wave")

The player auto-renders when it is the last expression in a notebook cell.

Audio Input Types

wavesurf accepts four audio source formats:

NumPy Arrays

display_audio(audio=numpy_array, sr=24000, title="From NumPy")

Requires the sr (sample rate) parameter. Audio is encoded to a base64 WAV data-URL and embedded directly in the HTML.

PyTorch Tensors

display_audio(audio=torch_tensor, sr=24000, title="From PyTorch")

Tensors are automatically detected at runtime and converted to numpy. PyTorch is not required at import time.

File Paths

display_audio(audio="path/to/audio.wav", title="From File")
display_audio(audio=Path("path/to/audio.flac"), title="Path Object")

Files are loaded via soundfile (WAV, FLAC, OGG, etc.). The sample rate is read from the file header.

URLs

display_audio(audio="https://example.com/audio.wav", title="From URL")

URLs are passed through directly to wavesurfer.js — the audio is streamed by the browser, not downloaded by Python.

Themes

Built-in Themes

from wavesurf import WaveSurfer, DARK, LIGHT

# Minimal dark theme (default)
WaveSurfer(audio=audio, sr=sr, theme=DARK)

# Clean light theme
WaveSurfer(audio=audio, sr=sr, theme=LIGHT)

# Themes can be referenced by name (string)
WaveSurfer(audio=audio, sr=sr, theme="light")

Theme Registry

Register custom themes for reuse across your project:

import wavesurf as ws

# Register a custom theme
my_brand = ws.Theme(
    wave_color=["#e63946", "#d62839"],
    progress_color=["#457b9d", "#1d3557"],
    background="#1d3557",
    play_button_style="shield",
    play_button_color="#f1faee",
    play_button_bg="linear-gradient(135deg, #e63946, #d62839)",
    title_color="#f1faee",
    time_color="rgba(241, 250, 238, 0.5)",
)
ws.themes.register(name="my-brand", theme=my_brand)

# Set as the global default — all subsequent calls use it automatically
ws.themes.enable(name="my-brand")

# Now every call uses my-brand without explicit theme= parameter
ws.display_audio(audio=audio, sr=sr, title="Branded Player")

# Or reference by name
ws.display_audio(audio=audio, sr=sr, theme="my-brand")

# List all registered themes
ws.themes.names()  # ['dark', 'light', 'my-brand']

Custom Themes

Create a custom theme by modifying any built-in:

from wavesurf import Theme, DARK

my_theme = DARK.replace(
    wave_color="#ff6b9d",
    progress_color="#00d9ff",
    background="#0a0e27",
    play_button_style="minimal",
)

WaveSurfer(audio=audio, sr=sr, theme=my_theme)

Or build one from scratch:

my_theme = Theme(
    wave_color=["#e63946", "#d62839"],
    progress_color=["#457b9d", "#1d3557"],
    cursor_color="#457b9d",
    bar_width=3,
    bar_gap=2,
    bar_radius=3,
    height=80,
    background="#1d3557",
    border="1px solid rgba(230, 57, 70, 0.15)",
    title_color="#f1faee",
    play_button_style="shield",
    play_button_color="#1d3557",
)

Theme Properties

Category Properties
Waveform wave_color, progress_color, cursor_color, bar_width, bar_gap, bar_radius, height
Container background, border, border_radius, padding, font_family
Title title_color, title_font_size, title_font_weight, title_marker_color, title_marker_shape
Play button play_button_style ("shield" / "circle" / "minimal"), play_button_color, play_button_bg, play_button_hover_glow
Time display time_color
Decorative top_accent, background_pattern, card_margin_bottom

Controls

Configure the control bar beneath the waveform:

from wavesurf import Controls, WaveSurfer

controls = Controls(
    show_play_button=True,       # play/pause button
    show_time=True,              # current time / duration
    show_volume=True,            # volume slider
    show_playback_rate=True,     # speed selector (0.5x - 2x)
    play_button_style="circle",  # override theme button style
    layout="bottom",             # "bottom" or "top"
)

WaveSurfer(audio=audio, sr=sr, controls=controls)

Button Styles

  • "shield" -- badge shape with gradient fill
  • "circle" -- rounded button (default for DARK/LIGHT themes)
  • "minimal" -- text-only play triangle

Events

Attach JavaScript callbacks to any of wavesurfer.js's 22 events:

from wavesurf import EventHandler, WaveSurfer

# Shorthand for the ready event
player = WaveSurfer(
    audio=audio, sr=sr,
    on_ready="console.log('Duration:', duration);",
)

# Multiple event handlers
player = WaveSurfer(
    audio=audio, sr=sr,
    events=[
        EventHandler.on_play(js="console.log('Playing');"),
        EventHandler.on_pause(js="console.log('Paused');"),
        EventHandler.on_audioprocess(js="console.log('Time:', currentTime);"),
        EventHandler.on_ready(js="console.log('Ready');", once=True),
    ],
)

All Events

Event Callback Parameters Description
on_init (none) Player initialized
on_load url Audio URL set
on_loading percent Loading progress
on_decode duration Audio decoded
on_ready duration Ready for playback
on_play (none) Playback started
on_pause (none) Playback paused
on_finish (none) Playback ended
on_audioprocess currentTime Playback progressing
on_timeupdate currentTime Time updated (throttled)
on_seeking currentTime Seeking to position
on_interaction newTime User interaction
on_click relativeX, relativeY Waveform clicked
on_dblclick relativeX, relativeY Waveform double-clicked
on_drag relativeX Dragging playback cursor
on_dragstart relativeX Drag started
on_dragend relativeX Drag ended
on_scroll visibleStartTime, visibleEndTime, scrollLeft, scrollRight Waveform scrolled
on_zoom minPxPerSec Zoom level changed
on_redraw (none) Waveform redrawn
on_redrawcomplete (none) Redraw finished
on_error error Error occurred
on_destroy (none) Player destroyed

Plugins

Register wavesurfer.js plugins using the Plugins factory:

from wavesurf import Plugins, WaveSurfer

player = WaveSurfer(
    audio=audio, sr=sr,
    plugins=[
        Plugins.timeline(height=20),
        Plugins.minimap(height=40, overlay=True),
        Plugins.regions(),
        Plugins.spectrogram(height=128, labels=True),
    ],
)

Built-in Plugin Factories

Plugins.timeline(height=20, time_interval=None, primary_label_interval=None,
                 secondary_label_interval=None, style=None)

Plugins.minimap(height=20, wave_color=None, progress_color=None, overlay=True)

Plugins.regions()

Plugins.spectrogram(height=128, labels=True, color_map=None)

Custom Plugins

from wavesurf import PluginConfig

custom = PluginConfig(
    name="MyPlugin",
    options={"someOption": "value"},
    js_source="https://cdn.example.com/my-plugin.js",
)

Layouts

Single Player

from wavesurf import display_audio

display_audio(audio=audio, sr=24000, title="Single Player")

Side-by-Side Comparison

from wavesurf import compare_audio

compare_audio(
    audio_dict={
        "Original": (audio_orig, 24000),
        "Processed": (audio_proc, 24000),
    },
    columns=1,  # vertical stack (default)
)

Values can be plain arrays (with a shared sr parameter) or (array, sr) tuples for mixed sample rates.

Grid Layout

from wavesurf import WaveSurfer, grid

players = [
    WaveSurfer(audio=a1, sr=24000, title="Track 1"),
    WaveSurfer(audio=a2, sr=24000, title="Track 2"),
    WaveSurfer(audio=a3, sr=24000, title="Track 3"),
    WaveSurfer(audio=a4, sr=24000, title="Track 4"),
]

grid(players=players, columns=2)  # 2x2 grid

Builder Pattern

Create variants of a player without modifying the original:

from wavesurf import WaveSurfer, EventHandler, Plugins, LIGHT

base = WaveSurfer(audio=audio, sr=24000, title="Base")

# Chain immutable builder methods
enhanced = (
    base
    .with_theme(theme=LIGHT)
    .with_options(bar_width=5, height=120)
    .with_events(EventHandler.on_ready(js="console.log('ready');"))
    .with_plugins(Plugins.timeline())
)

Available builder methods:

  • .with_options(**kwargs) -- add or override wavesurfer.js options
  • .with_theme(theme) -- switch the visual theme
  • .with_events(*handlers) -- append event handlers
  • .with_plugins(*configs) -- append plugins

All wavesurfer.js Options

Any wavesurfer.js option can be passed as a snake_case keyword argument:

WaveSurfer(
    audio=audio, sr=24000,
    bar_width=3, bar_gap=2, bar_radius=3,
    height=100, normalize=True,
    drag_to_seek=True, interact=True,
)
Python (snake_case) JavaScript (camelCase) Type
audio_rate audioRate float
auto_center autoCenter bool
auto_scroll autoScroll bool
autoplay autoplay bool
backend backend "WebAudio" / "MediaElement"
bar_align barAlign "top" / "bottom"
bar_gap barGap int
bar_height barHeight float
bar_radius barRadius int
bar_width barWidth int
blob_mime_type blobMimeType str
csp_nonce cspNonce str
cursor_color cursorColor str
cursor_width cursorWidth int
drag_to_seek dragToSeek bool / dict
duration duration float
fetch_params fetchParams dict
fill_parent fillParent bool
height height int / "auto"
hide_scrollbar hideScrollbar bool
interact interact bool
media_controls mediaControls bool
min_px_per_sec minPxPerSec int
normalize normalize bool
progress_color progressColor str / list[str]
render_function renderFunction str (raw JS)
sample_rate sampleRate int
split_channels splitChannels list[dict]
url url str
wave_color waveColor str / list[str]
width width int / str

Testing

wavesurf uses two test layers:

Python Unit Tests (103 tests)

uv run pytest tests/ --verbose

Covers options mapping, audio encoding/decoding, HTML generation, theme application, event handlers, core class behavior, and edge cases (XSS, empty audio, single samples, UID uniqueness).

Cypress Browser Tests

npm install
npm run test:e2e

Verifies waveform rendering, playback controls, visual regression across themes, comparison grid layouts, and all example pages (DOM structure + visual regression) in a real browser.

Both Layers

bash scripts/run_tests.sh

API Reference

Classes

Class Description
WaveSurfer Main player class with _repr_html_() for Jupyter display
Theme Frozen dataclass with 25+ visual styling properties
ThemeRegistry Named theme registry with global default and string-based selection
Controls Configuration for the player control bar
EventHandler Single event handler with JS callback and once flag
PluginConfig Configuration for a wavesurfer.js plugin
Plugins Factory class with static methods for common plugins
WaveSurferOptions All 31 wavesurfer.js options in snake_case

Functions

Function Description
display_audio(audio, sr, *, title, theme, controls, **options) Quick single-player display (theme accepts Theme object or string name)
compare_audio(audio_dict, sr, *, columns, theme, controls, **options) Multi-player comparison
compare(players, columns) Layout helper for WaveSurfer instance lists
grid(players, columns) Alias for compare() with 2-column default

Built-in Themes

Theme Style
DARK Dark background, purple accents, circle play button
LIGHT Light background, blue accents, circle play button

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wavesurf-0.1.0.tar.gz (63.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wavesurf-0.1.0-py3-none-any.whl (71.3 kB view details)

Uploaded Python 3

File details

Details for the file wavesurf-0.1.0.tar.gz.

File metadata

  • Download URL: wavesurf-0.1.0.tar.gz
  • Upload date:
  • Size: 63.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for wavesurf-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f72820d3af134262b83fae4ea632e5fcf28f906d8be8e201b8a3342c13150b00
MD5 47dcf43f6139344f367bbe44c1adb00a
BLAKE2b-256 bf0924a1ec4f3959666064a624320ea2d4c650c67e2d0efb70bfe5ef04dfa09e

See more details on using hashes here.

File details

Details for the file wavesurf-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: wavesurf-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 71.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for wavesurf-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b61d80b2a018be9005bad4e3c865f9c04c79e78defb447949f61029ef062ff98
MD5 7aef8288de9dcbc5ef3d381a6c34960f
BLAKE2b-256 72e40030d481e50a009e39b070d2614c856553a886b4acdde71dbd9a4fd3ca7b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page