npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@olilarkin/react-waveform

v0.0.1

Published

React audio waveform and synchronized spectrogram player.

Readme

@olilarkin/react-waveform

React component for an audio file player with a synchronized waveform and reassigned spectrogram display.

It is based on the audio analysis and WebAssembly DSP approach from @counterpoint-studio/audio-file-mcp-app, packaged for normal React applications.

Install

npm install @olilarkin/react-waveform
import {
  WaveformSpectrogramPlayer,
  type WaveformSpectrogramPlayerHandle,
} from "@olilarkin/react-waveform";
import "@olilarkin/react-waveform/style.css";

Usage

import { useRef, useState } from "react";
import {
  WaveformSpectrogramPlayer,
  type WaveformSpectrogramPlayerHandle,
} from "@olilarkin/react-waveform";
import "@olilarkin/react-waveform/style.css";

export function AudioInspector() {
  const [file, setFile] = useState<File | null>(null);
  const player = useRef<WaveformSpectrogramPlayerHandle>(null);

  return (
    <>
      <input
        type="file"
        accept="audio/*"
        onChange={(event) => setFile(event.currentTarget.files?.[0] ?? null)}
      />
      <WaveformSpectrogramPlayer
        ref={player}
        source={file}
        metadata={{ name: file?.name }}
        rangeSelection
        loopPlayback
        followPlayback={{ enabled: true, edgeFraction: 0.18, resumeDelayMs: 250 }}
        snapToMarkers
        defaultRange={{ startSeconds: 4, endSeconds: 12 }}
        locatorControls
        defaultLocators={{ leftSeconds: 4, rightSeconds: 12 }}
        markerLayers={[
          {
            id: "beats",
            color: "#00aaff",
            lane: "secondary",
            markers: [
              { id: "beat-1", seconds: 1.25, label: "1" },
              { id: "beat-2", seconds: 1.75, label: "2" },
            ],
          },
        ]}
        onReady={(info) => console.log("ready", info)}
        onRangeChange={(range) => console.log("range", range)}
        onError={(error) => console.error(error)}
      />
    </>
  );
}

source accepts a File, Blob, URL string, or URL. URL sources are fetched as a full blob in this first version.

Props

  • source?: File | Blob | string | URL | null
  • metadata?: { name?: string; sizeBytes?: number; mimeType?: string }
  • rangeSelection?: boolean defaults to false
  • loopPlayback?: boolean defaults to false
  • loopSelection?: boolean legacy alias for enabling both range selection and loop playback
  • timelineUnit?: "seconds" | "samples" | "percent" defaults to "seconds"
  • timelineOptions?: { visible?: boolean; height?: number | string; backgroundColor?: string; textColor?: string; borderColor?: string }
  • overlayOptions?: { hoverTextColor?: string; hoverTextBackgroundColor?: string; hoverTextShadow?: boolean; hoverTextShadowColor?: string; markerTop?: number | string; markerColor?: string; markerLabelColor?: string; markerLabelShadow?: boolean; markerLabelShadowColor?: string; locatorLabelColor?: string; locatorLabelShadow?: boolean; locatorLabelShadowColor?: string; rangeFillColor?: string; rangeFillOpacity?: number; rangeHandleColor?: string }
  • waveformDrawingMode?: "frequency" | "sections" | "solid" defaults to "solid"
  • sections?: Array<{ id: string | number; startSeconds?: number; endSeconds?: number; startNormalized?: number; endNormalized?: number; color: string; label?: string }> used when waveformDrawingMode is "sections"
  • spectrogramOptions?: { colorMap?: "inferno" | "jet" | "magma" | "viridis" | "grayscale"; contrast?: number; range?: number }
  • range?: { startSeconds: number; endSeconds: number } | null
  • defaultRange?: { startSeconds: number; endSeconds: number } | null
  • defaultViewMode?: "waveform" | "spectrum" | "both" defaults to "both"
  • autoPlay?: boolean defaults to false
  • followPlayback?: boolean | { enabled?: boolean; edgeFraction?: number; resumeDelayMs?: number } defaults to false
  • markerLayers?: WaveformMarkerLayer[] defaults to []
  • snapToMarkers?: boolean | { enabled?: boolean; radiusPx?: number } defaults to false
  • locators?: { leftSeconds: number; rightSeconds: number } | null
  • defaultLocators?: { leftSeconds: number; rightSeconds: number } | null
  • locatorControls?: boolean | { enabled?: boolean; color?: string; showLabels?: boolean; top?: number | string } defaults to false
  • appearance?: { accentColor?: string; textColor?: string; mutedTextColor?: string; faintTextColor?: string; borderColor?: string; backgroundColor?: string; playheadColor?: string; rangeFillColor?: string; rangeHandleColor?: string; spectrogramBackgroundColor?: string }
  • className?: string
  • style?: React.CSSProperties
  • onReady(info)
  • onPlaybackChange(playing)
  • onPositionChange(position)
  • onRangeChange(range)
  • onRegionChange(region)
  • onLocatorsChange(locators)
  • onMarkerMove(layerId, markerId, seconds)
  • onMetricsChange(metrics)
  • onError(error)

The imperative ref exposes play(), pause(), seek(seconds), zoomIn(), zoomOut(), resetZoom(), setRange(start,end), setRangeNormalized(start,end), setRangeCenterSpan(centerSeconds,spanSeconds), clearRange(), setLocators(left,right), clearLocators(), goToLeftLocator(), goToRightLocator(), setLoopRegion(start,end), and clearLoopRegion().

followPlayback is opt-in. When enabled and the user is zoomed in, playback shifts the visible viewport once the playhead leaves the configured edge margin. It is suppressed while the user interacts with the timeline and resumes after resumeDelayMs, which defaults to 250. edgeFraction defaults to 0.18 and is clamped to a practical range.

rangeSelection controls drag-to-select range gestures. Ranges can be controlled with range, initialized with defaultRange, or changed through the imperative ref. Use setRangeNormalized(start,end) for 0..1 file positions and setRangeCenterSpan(centerSeconds,spanSeconds) for centered selections. Existing ranges can be edited by dragging either edge or moved by dragging inside the range. loopPlayback controls whether playback loops the selected/programmatic range. They are independent so hosts can use selection as an analysis range without changing transport behavior.

appearance provides a typed styling surface for common colors. It maps to CSS variables on the root player, so advanced consumers can still override the same variables through style or external CSS.

Locators

locatorControls is opt-in. Locators can be controlled by the host with locators, initialized with defaultLocators, or changed through the imperative ref. The locator overlay is clipped and remapped through zoom like markers and the playhead.

const player = useRef<WaveformSpectrogramPlayerHandle>(null);

<WaveformSpectrogramPlayer
  ref={player}
  source={file}
  locatorControls={{ enabled: true, color: "#f97316" }}
  defaultLocators={{ leftSeconds: 8, rightSeconds: 24 }}
  onLocatorsChange={(locators) => console.log(locators)}
/>;

player.current?.goToLeftLocator();

Marker Layers

markerLayers lets consumers render any number of host-owned timeline overlays, such as detected beats, cue points, or analysis landmarks. Markers can be expressed in seconds or as a normalized 0...1 file position. The component maps them through the same viewport as the waveform, so they stay aligned while users zoom.

When snapToMarkers is enabled, seek taps and range endpoints snap to nearby markers using a screen-space radius. The default snap radius is 12px, so snap behavior remains local as users zoom.

<WaveformSpectrogramPlayer
  source={file}
  markerLayers={[
    {
      id: "beats",
      color: "#00aaff",
      lane: "secondary",
      markers: beats.map((seconds, index) => ({
        id: `beat-${index}`,
        seconds,
        label: String(index + 1),
      })),
    },
    {
      id: "sections",
      color: "#f97316",
      lane: "primary",
      markers: [{ id: "chorus", normalized: 0.42, label: "Chorus" }],
    },
  ]}
/>

Styling

Import @olilarkin/react-waveform/style.css. The component is scoped under .wr-player and defaults to a compact 240px height.

Useful CSS variables:

.wr-player {
  --wr-accent: #38bdf8;
  --wr-timeline-height: 30px;
  --wr-timeline-background: #d6dae1;
  --wr-timeline-color: #15171a;
  --wr-hover-text-color: #fff;
  --wr-hover-text-shadow: none;
  --wr-marker-top: 0px;
  --wr-marker-label-color: currentColor;
  --wr-marker-label-shadow: none;
  --wr-locator-label-color: currentColor;
  --wr-locator-label-shadow: none;
  --wr-range-fill-color: #38bdf8;
  --wr-range-fill-opacity: 22%;
  --wr-range-handle: #38bdf8;
  --wr-spectrogram-height: 60px;
  --wr-color-text: #111;
  --wr-color-text-muted: #666;
  --wr-color-border: #dcdcdc;
  --wr-color-background: #ececec;
}

WASM DSP

The package includes an inlined WebAssembly DSP module for FFT, loudness, and spectrogram rendering, with a JavaScript fallback for environments that block WebAssembly compilation.

Regenerate DSP artifacts only when the C/vendor DSP sources change:

pnpm run build:dsp

This requires an active Emscripten SDK environment.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm dev

License

ISC. Portions of the audio engine are adapted from @counterpoint-studio/audio-file-mcp-app; see NOTICE.