TONE3000 MUSHRA

June 1, 2026 · View on GitHub

npm version

A React component for running web-based MUSHRA-style listening tests on audio samples.

You provide audio as URLs and a callback for persistence. t3k-mushra handles the rest: gapless Web Audio playback with instant A/B switching, a draggable loop region, blind randomized ordering, 0–100 sliders, navigation, progress, and resumable sessions.

Install

npm install t3k-mushra

react and react-dom (18 or 19) are peer dependencies. Import the stylesheet once, anywhere in your app:

import 't3k-mushra/style.css';

Demo

The repo is a Vite app that runs a complete example test using runtime-synthesized audio, so it works offline with no committed media:

npm install
npm run dev

Open the printed URL. The demo persists results to localStorage and lets you download them as JSON.

Data model

A test is a two-level structure:

  • MushraTest{ id?, title?, items }.
  • MushraItem — a group of pages shown together (e.g. one "tone"): { id, title?, description?, pages }. Each item is a row on the overview.
  • MushraPage — one rating screen: { id, description?, reference?, stimuli }. A page has an optional reference plus the stimuli to rate.
  • MushraStimulus — a single audio sample: { id, url }. The id is the ground-truth key used in results and is never shown to the listener.
  • MushraReference — an optional explicit reference: { url, label? }. The label defaults to "Reference".

Within a page, stimuli are presented in a blind, randomized order (labeled A, B, C…). The order is deterministic per participantId, so a participant always sees the same layout. This makes sessions resumable and analysis reproducible.

Flow

By default the participant sees:

  1. An instructions dialog (showWelcome).
  2. An overview/index page (showIndex) listing every item with its status and overall progress, plus a Start/Continue button. They can leave and resume here at any time.
  3. A page screen: reference + blind stimuli with 0–100 sliders, a loop bar, and a numbered stepper. "Submit & next" saves the page and advances. On the final page of an item the button reads "Submit" and returns to the overview — unless every item is complete, in which case it goes to the completion screen. A "Back to overview" link is available at any time.
  4. A completion screen (or your renderComplete, if provided) once every page of every item is submitted.

Set showIndex={false} to skip the landing page and step participants linearly through every page, ending on the completion screen.

Usage

import { Mushra } from 't3k-mushra';
import type { MushraTest, MushraPageResult } from 't3k-mushra';

const test: MushraTest = {
  id: 'codec-eval-2026',
  title: 'Codec evaluation',
  items: [
    {
      id: 'guitar-riff',
      title: 'Sample 1',
      description: 'Rate how closely each version matches the reference.',
      pages: [
        {
          id: 'clip-1',
          reference: { url: '/audio/guitar/reference.wav', label: 'Reference' },
          stimuli: [
            { id: 'opus-128', url: '/audio/guitar/opus-128.wav' },
            { id: 'mp3-128', url: '/audio/guitar/mp3-128.wav' },
            { id: 'anchor-3500', url: '/audio/guitar/anchor.wav' },
            { id: 'hidden-ref', url: '/audio/guitar/reference.wav' },
          ],
        },
      ],
    },
    // ...more items
  ],
};

function Study() {
  return (
    <Mushra
      test={test}
      participantId="participant-42"
      onSubmitPage={async (result: MushraPageResult) => {
        await fetch('/api/mushra', { method: 'POST', body: JSON.stringify(result) });
      }}
      onComplete={(results) => {
        console.log('Done!', results);
      }}
    />
  );
}

Resuming a session

Pass previously saved results back in via initialResults. The component starts the participant on the first unanswered page and marks completed ones in the stepper:

<Mushra test={test} participantId={id} initialResults={savedResults} onSubmitPage={save} />

<Mushra /> props

PropTypeDefaultDescription
testMushraTestThe test definition (required).
participantIdstringSeeds the deterministic blind order. Recommended.
initialResultsMushraPageResult[][]Previously saved results, for resuming.
onSubmitPage(r) => void | Promise<void>Called per page submit. Return a promise to show a spinner.
onComplete(results) => voidCalled once after the final page.
randomizeStimulusOrderbooleantrueBlind-shuffle stimuli within each page.
randomizeItemOrderbooleanfalseShuffle item order per participant.
requireAllRatedbooleantrueWarn before submitting if a slider was untouched.
showIndexbooleantrueShow the overview/index landing page listing items + progress.
showWelcomebooleantrueShow the instructions dialog first.
showWalkthroughbooleantrueShow the guided interface tour.
instructionsMushraInstructionssensible defaultsCustom welcome copy.
walkthroughStepsMushraWalkthroughStep[]sensible defaultsCustom tour steps.
qualityBandsMushraQualityBand[]MUSHRA bandsLabels shown beneath each slider.
labelsMushraLabelssensible defaultsOverride UI strings.
renderComplete(results) => ReactNodebuilt-in screenCustom completion screen.
classNamestringExtra class on the root element.

Results shape

onSubmitPage receives one MushraPageResult per page:

interface MushraPageResult {
  itemId: string;
  pageId: string;
  submittedAt: string; // ISO timestamp
  participantId?: string;
  ratings: {
    stimulusId: string;  // your ground-truth id
    blindLabel: string;  // what the listener saw, e.g. "C"
    score: number;       // 0–100
    rated: boolean;      // false if left at the default
  }[];
}

Theming

All visuals are driven by CSS custom properties scoped to .mushra-root (see src/lib/styles/theme.css). Override them globally or on a wrapper to re-theme:

.mushra-root {
  --mushra-accent: #8b5cf6;
  --mushra-radius: 0.375rem;
  --mushra-scale-from: #b91c1c; /* slider gradient bottom */
  --mushra-scale-to: #15803d;   /* slider gradient top */
}

Lower-level building blocks

For a custom UI, the internals are exported too:

  • useAudioEngine(sources, resetOnPlay) — the gapless, loopable Web Audio engine.
  • blindConditions(stimuli, { participantId, trialId, randomize }) — deterministic A/B/C labeling.
  • PreferencesProvider / usePreferences — loop region and reset-on-switch preferences.
  • MushraIndex, TrialView, LoopBar, WelcomeDialog, Walkthrough, Button.
  • clampLoopRegion, seededShuffle, fnv1a32, mulberry32.

Project layout

src/
  lib/                 # the reusable library
    Mushra.tsx         # the orchestrator component (main entry)
    types.ts           # public data model
    hooks/             # useAudioEngine (Web Audio)
    components/        # TrialView, LoopBar, dialogs, Button
    context/           # PreferencesProvider
    internal/          # prng, shuffle, loop-region, cx
    styles/            # theme.css + scoped CSS modules
  demo/                # demo-only: synthesized audio + localStorage store
  App.tsx              # demo app wiring

Building the package

npm run build bundles src/lib (the published library) into dist/ via Vite library mode (vite.lib.config.ts): an ES module (dist/index.js), a single stylesheet (dist/style.css), and type declarations (dist/index.d.ts). react, react-dom, and lucide-react are externalized. npm run build:demo builds the demo app into dist-demo/.

Browser support

Requires the Web Audio API (all modern browsers). Audio is fetched and decoded with fetch + decodeAudioData, so stimulus URLs must be CORS-accessible.

License

MIT