t3k-mushra
v0.1.0
Published
Web-based MUSHRA-style audio listening tests.
Maintainers
Readme
TONE3000 MUSHRA
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-mushrareact 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 devOpen 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 }. Theidis the ground-truth key used in results and is never shown to the listener.MushraReference— an optional explicit reference:{ url, label? }. Thelabeldefaults 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:
- An instructions dialog (
showWelcome). - 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. - 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.
- 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
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| test | MushraTest | — | The test definition (required). |
| participantId | string | — | Seeds the deterministic blind order. Recommended. |
| initialResults | MushraPageResult[] | [] | Previously saved results, for resuming. |
| onSubmitPage | (r) => void \| Promise<void> | — | Called per page submit. Return a promise to show a spinner. |
| onComplete | (results) => void | — | Called once after the final page. |
| randomizeStimulusOrder | boolean | true | Blind-shuffle stimuli within each page. |
| randomizeItemOrder | boolean | false | Shuffle item order per participant. |
| requireAllRated | boolean | true | Warn before submitting if a slider was untouched. |
| showIndex | boolean | true | Show the overview/index landing page listing items + progress. |
| showWelcome | boolean | true | Show the instructions dialog first. |
| showWalkthrough | boolean | true | Show the guided interface tour. |
| instructions | MushraInstructions | sensible defaults | Custom welcome copy. |
| walkthroughSteps | MushraWalkthroughStep[] | sensible defaults | Custom tour steps. |
| qualityBands | MushraQualityBand[] | MUSHRA bands | Labels shown beneath each slider. |
| labels | MushraLabels | sensible defaults | Override UI strings. |
| renderComplete | (results) => ReactNode | built-in screen | Custom completion screen. |
| className | string | — | Extra 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 wiringBuilding 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.
