Documentation
- Introduction
- Quick setup
- Configuration shape
- Data
- Views
- Features
- Theming
- IDE Support
- Keyboard and loop controls
- Configuration requirements
Introduction
TrackSwitch is a web-based multitrack audio player for scientific audio results. It allows for playback of multiple audio tracks in one easy-to-use and very customizable interface. This page is the reference for all possible configuration keys that can be used to set up a trackswitch player.
The Tutorials & Use Cases pages explain the main concepts of the player and show concrete and complete configuration examples. Read them for further usage examples.
Quick setup
In the first row, select the player type. In the second row, select the integration method.
<script src="dist/js/trackswitch.js"></script>
<trackswitch-player>
<script type="application/json">
{
"media": {
"drums": { "type": "audio", "src": "drums.mp3", "title": "Drums" },
"bass": { "type": "audio", "src": "bass.mp3", "title": "Bass" },
"synth": { "type": "audio", "src": "synth.mp3", "title": "Synth" }
},
"views": [
{
"type": "navigationBar",
"controls": ["playback", "globalVolume", "markerNavigation", "presets", "timer", "seekBar"]
},
{ "type": "waveform", "tracks": "audible" },
{
"type": "trackList",
"tracks": ["drums", "bass", "synth"],
"trackVolumeControls": true,
"trackPanControls": "balance"
}
]
}
</script>
</trackswitch-player>
import {
defineTrackswitchDefaultElement,
type TrackSwitchInit,
} from "trackswitch";
const config: TrackSwitchInit = {
media: {
drums: { type: "audio", src: "drums.mp3", title: "Drums" },
bass: { type: "audio", src: "bass.mp3", title: "Bass" },
synth: { type: "audio", src: "synth.mp3", title: "Synth" },
},
views: [
{
type: "navigationBar",
controls: ["playback", "globalVolume", "markerNavigation", "presets", "timer", "seekBar"],
},
{ type: "waveform", tracks: "audible" },
{
type: "trackList",
tracks: ["drums", "bass", "synth"],
trackVolumeControls: true,
trackPanControls: "balance",
},
],
};
defineTrackswitchDefaultElement();
const player = document.querySelector("trackswitch-player") as HTMLElement & {
config: TrackSwitchInit;
};
player.config = config;
import { useMemo } from "react";
import type { TrackSwitchInit } from "trackswitch";
import { TrackSwitchPlayer } from "trackswitch/react";
export function ExamplePlayer() {
const config = useMemo<TrackSwitchInit>(() => {
return {
media: {
drums: { type: "audio", src: "drums.mp3", title: "Drums" },
bass: { type: "audio", src: "bass.mp3", title: "Bass" },
synth: { type: "audio", src: "synth.mp3", title: "Synth" },
},
views: [
{
type: "navigationBar",
controls: ["playback", "globalVolume", "markerNavigation", "presets", "timer", "seekBar"],
},
{ type: "waveform", tracks: "audible" },
{
type: "trackList",
tracks: ["drums", "bass", "synth"],
trackVolumeControls: true,
trackPanControls: "balance",
},
],
};
}, []);
return <TrackSwitchPlayer config={config} className="trackswitch-host" />;
}
<script setup lang="ts">
import type { TrackSwitchInit } from "trackswitch";
import { TrackSwitchPlayer } from "trackswitch/vue";
const config: TrackSwitchInit = {
media: {
drums: { type: "audio", src: "drums.mp3", title: "Drums" },
bass: { type: "audio", src: "bass.mp3", title: "Bass" },
synth: { type: "audio", src: "synth.mp3", title: "Synth" },
},
views: [
{
type: "navigationBar",
controls: ["playback", "globalVolume", "markerNavigation", "presets", "timer", "seekBar"],
},
{ type: "waveform", tracks: "audible" },
{
type: "trackList",
tracks: ["drums", "bass", "synth"],
trackVolumeControls: true,
trackPanControls: "balance",
},
],
};
</script>
<template>
<TrackSwitchPlayer :config="config" class="trackswitch-host" />
</template>
<script lang="ts">
import {
useTrackswitch,
type TrackswitchSvelteOptions,
} from "trackswitch/svelte";
const options: TrackswitchSvelteOptions = {
config: {
media: {
drums: { type: "audio", src: "drums.mp3", title: "Drums" },
bass: { type: "audio", src: "bass.mp3", title: "Bass" },
synth: { type: "audio", src: "synth.mp3", title: "Synth" },
},
views: [
{
type: "navigationBar",
controls: ["playback", "globalVolume", "markerNavigation", "presets", "timer", "seekBar"],
},
{ type: "waveform", tracks: "audible" },
{
type: "trackList",
tracks: ["drums", "bass", "synth"],
trackVolumeControls: true,
trackPanControls: "balance",
},
],
},
};
</script>
<trackswitch-player use:useTrackswitch={options} class="trackswitch-host" />
<script src="dist/js/trackswitch.js"></script>
<trackswitch-player>
<script type="application/json">
{
"media": {
"score": { "type": "musicxml", "src": "score.musicxml" },
"notes": { "type": "midi", "src": "notes.mid" },
"takeA": {
"type": "audio",
"src": "take-a.wav",
"srcSynchronized": { "src": "take-a-synced.wav" },
"title": "Take A"
},
"takeB": {
"type": "audio",
"src": "take-b.wav",
"srcSynchronized": { "src": "take-b-synced.wav" },
"title": "Take B"
}
},
"alignment": {
"src": "alignment.csv",
"referenceTimeline": "score",
"timelines": {
"score": "measure",
"notes": "midi_seconds",
"takeA": "take_a_seconds",
"takeB": "take_b_seconds"
},
"outsideCoverage": "hold"
},
"markers": {
"sections": {
"src": "sections.csv",
"timeline": "score",
"timeCol": "measure",
"labelCol": "section"
}
},
"views": [
{
"type": "navigationBar",
"controls": ["playback", "globalVolume", "markerNavigation", "looping", "sync", "timer", "seekBar"]
},
{ "type": "sheetMusic", "mediaID": "score" },
{ "type": "midi", "mediaID": "notes", "timer": true },
{
"type": "waveform",
"tracks": ["takeA"],
"timer": true,
"alignedPlayhead": true,
"markerLayers": [
{ "set": "sections", "color": "#ed8c01" },
{ "set": "alignment", "color": "#777", "foldToReference": true }
]
},
{ "type": "waveform", "tracks": ["takeB"], "timer": true },
{ "type": "warpingMatrix", "x": "takeA", "y": "takeB" },
{ "type": "trackList", "tracks": ["takeA", "takeB"], "soloGroup": 0 }
]
}
</script>
</trackswitch-player>
import {
defineTrackswitchDefaultElement,
type TrackSwitchInit,
} from "trackswitch";
const config: TrackSwitchInit = {
media: {
score: { type: "musicxml", src: "score.musicxml" },
takeA: { type: "audio", src: "take-a.wav", title: "Take A" },
takeB: { type: "audio", src: "take-b.wav", title: "Take B" },
},
alignment: {
src: "alignment.csv",
referenceTimeline: "score",
timelines: {
score: "measure",
takeA: "take_a_seconds",
takeB: "take_b_seconds",
},
outsideCoverage: "hold",
},
views: [
{
type: "navigationBar",
controls: ["playback", "markerNavigation", "looping", "sync", "timer", "seekBar"],
},
{ type: "sheetMusic", mediaID: "score" },
{ type: "waveform", tracks: ["takeA"], alignedPlayhead: true },
{ type: "waveform", tracks: ["takeB"], alignedPlayhead: true },
{ type: "warpingMatrix", x: "takeA", y: "takeB" },
{ type: "trackList", tracks: ["takeA", "takeB"], soloGroup: 0 },
],
};
defineTrackswitchDefaultElement();
const player = document.querySelector("trackswitch-player") as HTMLElement & {
config: TrackSwitchInit;
};
player.config = config;
import { useMemo } from "react";
import type { TrackSwitchInit } from "trackswitch";
import { TrackSwitchPlayer } from "trackswitch/react";
export function AlignedPlayer() {
const config = useMemo<TrackSwitchInit>(() => ({
media: {
score: { type: "musicxml", src: "score.musicxml" },
takeA: { type: "audio", src: "take-a.wav", title: "Take A" },
takeB: { type: "audio", src: "take-b.wav", title: "Take B" },
},
alignment: {
src: "alignment.csv",
referenceTimeline: "score",
timelines: {
score: "measure",
takeA: "take_a_seconds",
takeB: "take_b_seconds",
},
},
views: [
{
type: "navigationBar",
controls: ["playback", "markerNavigation", "looping", "sync", "timer", "seekBar"],
},
{ type: "sheetMusic", mediaID: "score" },
{ type: "waveform", tracks: ["takeA"], alignedPlayhead: true },
{ type: "waveform", tracks: ["takeB"], alignedPlayhead: true },
{ type: "trackList", tracks: ["takeA", "takeB"], soloGroup: 0 },
],
}), []);
return <TrackSwitchPlayer config={config} />;
}
<script setup lang="ts">
import type { TrackSwitchInit } from "trackswitch";
import { TrackSwitchPlayer } from "trackswitch/vue";
const config: TrackSwitchInit = {
media: {
score: { type: "musicxml", src: "score.musicxml" },
takeA: { type: "audio", src: "take-a.wav", title: "Take A" },
takeB: { type: "audio", src: "take-b.wav", title: "Take B" },
},
alignment: {
src: "alignment.csv",
referenceTimeline: "score",
timelines: {
score: "measure",
takeA: "take_a_seconds",
takeB: "take_b_seconds",
},
},
views: [
{
type: "navigationBar",
controls: ["playback", "markerNavigation", "looping", "sync", "timer", "seekBar"],
},
{ type: "sheetMusic", mediaID: "score" },
{ type: "waveform", tracks: ["takeA"], alignedPlayhead: true },
{ type: "waveform", tracks: ["takeB"], alignedPlayhead: true },
{ type: "trackList", tracks: ["takeA", "takeB"], soloGroup: 0 },
],
};
</script>
<template>
<TrackSwitchPlayer :config="config" />
</template>
<script lang="ts">
import { useTrackswitch } from "trackswitch/svelte";
import type { TrackSwitchInit } from "trackswitch";
const config: TrackSwitchInit = {
media: {
score: { type: "musicxml", src: "score.musicxml" },
takeA: { type: "audio", src: "take-a.wav", title: "Take A" },
takeB: { type: "audio", src: "take-b.wav", title: "Take B" },
},
alignment: {
src: "alignment.csv",
referenceTimeline: "score",
timelines: {
score: "measure",
takeA: "take_a_seconds",
takeB: "take_b_seconds",
},
},
views: [
{
type: "navigationBar",
controls: ["playback", "markerNavigation", "looping", "sync", "timer", "seekBar"],
},
{ type: "sheetMusic", mediaID: "score" },
{ type: "waveform", tracks: ["takeA"], alignedPlayhead: true },
{ type: "waveform", tracks: ["takeB"], alignedPlayhead: true },
{ type: "trackList", tracks: ["takeA", "takeB"], soloGroup: 0 },
],
};
</script>
<trackswitch-player use:useTrackswitch= />
Configuration shape
The configuration uses these top-level keys:
| Key | Required | Description |
|---|---|---|
media |
yes | Contains named audio, MIDI, and MusicXML resources. It must contain at least one audio entry. |
views |
yes | Contains visual surfaces in their shown order. It must contain at least one view. |
alignment |
no | Contains correspondence data for two or more timelines. |
markers |
no | Contains named CSV files with annotation markers. |
presets |
no | Contains named groups of audio tracks. |
features |
no | Controls player behavior and user interaction. |
css |
no | Overrides theming tokens for the whole player. |
$schema |
no | Names the JSON Schema so editors can help. See IDE Support. |
The player rejects unknown keys.
Data
Data keys define the player model. Views use the data IDs.
media
media maps stable IDs to source entries. Audio entries create playable tracks.
MIDI, MusicXML, and image entries are visual resources. They do not create audio output.
{
"media": {
"violin": {
"type": "audio",
"src": "violin.mp3",
"title": "Violin",
"imageID": "violinFig",
"solo": true,
"volume": 0.9,
"pan": -0.2,
"startOffsetMs": 100,
"endOffsetMs": 50
},
"notes": { "type": "midi", "src": "notes.mid" },
"score": { "type": "musicxml", "src": "score.musicxml" },
"violinFig": { "type": "image", "src": "violin.png" },
"spectrogram": { "type": "image", "src": "spectrogram.png" }
}
}
Audio media properties:
| Property | Type | Default | Description |
|---|---|---|---|
type |
"audio" |
- | Identifies a playable audio track. |
src |
string |
- | Specifies the audio file URL. |
title? |
string |
media ID | Specifies the name in track lists. |
imageID? |
string |
none | Identifies an image media entry shown by perTrackImage while this track is soloed. |
solo? |
boolean |
false |
Specifies the initial active state of the track. |
volume? |
number |
1 |
Specifies the initial track volume. |
pan? |
number |
0 |
Specifies the initial stereo pan. |
startOffsetMs? |
number |
0 |
Trims or pads the start. A positive value trims audio. A negative value adds silence. |
endOffsetMs? |
number |
0 |
Trims or pads the end. A positive value trims audio. A negative value adds silence. |
srcSynchronized? |
object |
none | Specifies optional audio pre-warped onto the reference timeline, played by the sync control. |
timelineUnit? |
string |
native unit | Specifies the unit this medium’s positions read out in. See timeline units. |
css? |
object |
none | Overrides theming tokens for this track’s row. |
srcSynchronized identifies a version of the recording that is already warped
onto the reference timeline, which is what the sync control plays:
{
"srcSynchronized": { "src": "violin-synchronized.wav" }
}
srcSynchronized accepts its own startOffsetMs and endOffsetMs. They apply
only to the synchronized file and do not inherit from the media entry, because a
time-warped rendition carries its silence differently from the original:
{
"src": "violin.wav",
"startOffsetMs": 600,
"endOffsetMs": 300,
"srcSynchronized": {
"src": "violin-synchronized.wav",
"startOffsetMs": 300,
"endOffsetMs": 6900
}
}
MIDI, MusicXML, and image entries accept type, src, and timelineUnit:
{
"media": {
"notes": { "type": "midi", "src": "notes.mid", "timelineUnit": "ticks" },
"score": { "type": "musicxml", "src": "score.musicxml" },
"spectrogram": { "type": "image", "src": "spectrogram.png" }
}
}
An image media entry can use its own timeline. Add a column for the image to alignment.timelines.
Then add an image view that uses the image mediaID. The player seeks through the alignment instead of a linear position.
This configuration supports a spectrogram, scanned page, or structure plot with a nonuniform time axis.
alignment
If media do not share one timeline, use alignment. The CSV contains corresponding positions from each abstract timeline.
{
"alignment": {
"src": "alignment.csv",
"referenceTimeline": "score",
"timelines": {
"score": "measure",
"notes": "midi_seconds",
"takeA": "take_a_seconds",
"takeB": "take_b_samples"
},
"outsideCoverage": "hold",
"duplicatePlacements": "average"
}
}
| Property | Type | Default | Description |
|---|---|---|---|
src |
string |
- | Specifies the CSV file with timing correspondences. |
referenceTimeline |
string |
- | Specifies the timeline for the main timer and shared navigation. |
timelines |
Record<string, string> |
- | Maps each timeline ID to a CSV column name. |
outsideCoverage? |
"hold" \| "extrapolate" \| "error" |
"error" |
Controls projection outside the CSV coverage. |
duplicatePlacements? |
"first" \| "average" \| "error" |
"first" |
Controls rows that map to the same timeline position. |
Timeline IDs usually match media IDs. The value of referenceTimeline must be a key in timelines.
Repeated and revisited positions
A column may place the same position on several rows, and it may step backwards. Both happen when one performance plays a repeat that another one skips: the performance that plays the passage once sits at the same moment for both passes, and its column returns to that moment when the repeat begins.
Placements are therefore not required to increase. The player reads each column
as a path through the alignment rather than as a sorted list, and it splits that
path wherever a column steps backwards. Projecting onto such a timeline stays
unambiguous — its playhead simply jumps back when the repeat starts. Projecting
from it is ambiguous, because one of its positions belongs to two moments
elsewhere, and duplicatePlacements decides that case:
first(default) — Uses the earliest matching row, so an ambiguous position resolves to the first pass.average— Blends the matching rows. Useful when duplicates are a quantization plateau, such as a measure column that holds a value for several frames, rather than a genuine repeat.error— Rejects a CSV that places a timeline twice at one position. Use it while developing a CSV that is meant to be one-to-one.
Only the ambiguous direction is affected; the interpolation on either side of a repeat is unchanged.
Put a performance that plays every repeat — or an abstract unfolded timeline —
on referenceTimeline. The selected audio performance drives playback by
projecting its local clock onto that reference. If it skips an occurrence, the
reference position jumps over that occurrence; if it includes the occurrence,
playback visits it normally.
Where the reference stands still
A reference column may hold one value across a stretch of another timeline. A score ends at its last measure while the recording of it keeps sounding for several seconds; a measure column that was tracked at a coarse resolution holds each measure for many audio frames. Over such a stretch the reference position is a lossy summary — it does not move, even though playback does.
The player therefore carries the position at full resolution on the timeline it was established on: the playing track’s own clock, or the surface a seek landed on. Every surface is placed from that, projecting between timelines directly, so a waveform head and a piano roll run smoothly through a stretch the reference holds. Only the surfaces that genuinely have nothing further to show — the score cursor, the reference timer and its seek bar — stand still with it.
Arriving from the reference itself has no such position to work from, so it resolves to the first placement of the held value: clicking the last measure of a score, or a marker on it, seeks to the moment that measure starts sounding, and playback runs on through the rest of the stretch from there.
Timeline units
A timeline belongs to its medium, and so does the meaning of the numbers in its column: media.timelineUnit defines it. When the player loads the alignment, it converts the values.
In the example above, take_b_samples holds sample indices because the takeB media entry declares "timelineUnit": "samples". The converted values use the playback coordinates of the media item, so annotations can use the unit from the source analysis:
| Media type | Native unit | Also accepts | Converted using |
|---|---|---|---|
audio |
seconds |
samples |
the sample rate in the file’s container header |
midi |
seconds |
ticks |
the file header, including tempo changes |
musicxml |
measures |
- | - |
image |
percent (of image width) |
pixels |
the natural width of the decoded image |
If you omit timelineUnit, the medium uses the native unit of its type.
A timeline without a media entry has no unit to declare and no conversion to do. Its values stand as they are.
Sample indices refer to the file as it was authored. The rate is read from the container header of the source (WAV, FLAC, Ogg, MP3, MP4/M4A), not from the decoded buffer, whose rate is the audio context’s output rate. A samples column on a source whose header cannot be read raises an error.
Units without an alignment
timelineUnit does not need an alignment block. Without one every medium shares a single timeline, so the declaration is purely a readout: a surface prints the shared position in the unit of the medium it draws.
{
"media": {
"takeA": { "type": "audio", "src": "take-a.wav", "timelineUnit": "samples" },
"takeB": { "type": "audio", "src": "take-b.wav" }
}
}
The takeA waveform reads out in samples of its own file, the takeB waveform stays in HH:MM:SS.mmm, and the navigation bar timer takes the first medium that declares a unit — takeA here.
See Timeline Units for a worked example.
Coverage and playback
Alignment data covers only the span of its CSV rows. outsideCoverage controls positions outside this span:
hold— Holds positions at the nearest covered point. The player dims surfaces without data (ts-out-of-coverage). Media playback continues.extrapolate— Continues the slope of the outermost segment. Each timeline keeps a value, but values outside coverage are extrapolations.error— Throws an error for projection outside coverage. Use this value during CSV development.
The reference timeline controls the player position, duration, loops, and navigation. It may be an abstract timeline with no media entry. The selected audio performance supplies the physical playback clock: its local time is projected onto the reference after every update.
When media backs the reference timeline, that medium’s playable extent defines the player bounds. Audio start and end offsets therefore affect the reference duration. An abstract reference instead uses the extent covered by its alignment CSV column. A trimmed reference is rebased to zero in the player readout; the removed source-time offset remains relevant only when alignment rows are mapped onto the cropped audio.
The main timer therefore always shows the reference timeline. With hold, a
surface outside alignment coverage stays at its nearest covered position and is
dimmed.
markers
Markers add sparse positions to the player. Use them for musical sections, analysis events, lyrics, beats, or other meaningful positions.
{
"markers": {
"sections": {
"src": "sections.csv",
"timeline": "takeA",
"timeCol": "start",
"labelCol": "label"
}
}
}
| Property | Type | Default | Description |
|---|---|---|---|
src |
string |
- | Specifies the CSV file with marker data. |
timeline? |
string |
reference timeline | Specifies the timeline for timeCol. |
timeCol |
string |
- | Specifies the CSV column with marker positions, in the unit of that timeline. |
labelCol? |
string |
none | Specifies the CSV column with marker labels. |
timeCol values are read in the timeline unit of the timeline they belong to, the same unit that timeline’s alignment column and readout use. A set on a timeline whose medium declares "timelineUnit": "samples" is authored in sample indices; one on a musicxml timeline is authored in measure numbers. Without an alignment block the set sits on the single implicit timeline, and its values are read in the unit the player reads out.
Previous and next navigation uses the marker sets a view currently shows. A set becomes a navigation target through a markerLayers entry, so a set that no view draws is not navigable.
On a waveform with tracks: "audible", the layers follow the audible tracks. Track selection and track-volume changes therefore update the available navigation targets immediately. Layers on a fixed-track waveform, a midi view, or an image keep their markers regardless of solo state.
The jump and loop-point fields search all annotation sets. Visibility and audible track state do not affect these searches.
Each result identifies the marker set, numerical ID, label, and reference-timeline position.
Each set numbers its annotation markers in CSV order, starting at 1. Each set also contains hidden boundary IDs.
ID 0 sits at the start of the player timeline. ID N+1 sits at its end, where N is the number of annotation markers.
Previous and next navigation can use boundary markers. Marker layers and searchable fields do not show them.
If a set does not have labelCol, the marker hover text shows the numerical ID.
Views show marker sets through markerLayers:
{
"type": "waveform",
"tracks": ["takeA"],
"markerLayers": [
{ "set": "sections", "color": "#ed8c01", "line": "dashed", "lineWidth": 2 },
{ "set": "alignment", "color": "#777", "foldToReference": true }
]
}
set identifies a marker set. The special alignment set exists only with an alignment block.
foldToReference draws connectors between the current view timeline and the reference timeline. These connectors show warping points.
Marker layer properties:
| Property | Type | Default | Description |
|---|---|---|---|
set |
string |
- | Specifies a marker set ID or the implicit alignment set. |
color? |
string |
current color | Specifies the marker color. |
line? |
"solid" \| "dashed" |
"dashed" |
Specifies the marker line style. |
lineWidth? |
number |
1 |
Specifies the marker line width in CSS pixels. |
opacity? |
number |
--ts-marker-opacity |
Specifies the resting opacity from 0 to 1. |
foldToReference? |
boolean |
false |
Shows connectors from the timeline to the reference where applicable. |
presets
Presets define named track groups. They support selection of a full mix, instrument family, or analysis condition.
{
"presets": {
"all": { "label": "All tracks", "tracks": ["violin", "bass", "drums"] },
"rhythm": { "label": "Rhythm", "tracks": ["bass", "drums"] }
}
}
| Property | Type | Default | Description |
|---|---|---|---|
label? |
string |
preset ID | Specifies the name in the user interface. |
tracks |
string[] |
- | Specifies the audio media IDs in the preset. |
A preset that names several tracks of one soloGroup narrows it to the first of
them, because such a selection plays one track at a time. With an alignment block that applies to
the whole player, so a preset resolves to a single selected timeline — one track, or the
tracks it names within one non-exclusive list.
Views
The player shows views in declaration order. Each view has a type. Most views accept an optional css block of theming tokens.
The image, perTrackImage, waveform, and midi views can provide seekable surfaces. They accept markerLayers.
The two image views also accept seekMarginLeft and seekMarginRight, because a supplied picture can carry axes or whitespace around its plot area. The player draws waveforms and piano rolls itself, so those surfaces always span their full width.
image
An image view shows one static image, such as cover art, a diagram, or a time-aligned illustration.
Every image is a media entry, so the view names one with mediaID:
{
"type": "image",
"mediaID": "cover",
"seekable": true,
"seekMarginLeft": 3,
"seekMarginRight": 3,
"markerLayers": [{ "set": "sections", "line": "solid" }]
}
If the entry has a column in alignment.timelines, the player projects its playhead, seek positions, and marker layers.
Thus, an image with a nonuniform time axis can align with other media:
{ "type": "image", "mediaID": "spectrogram", "seekable": true }
Without such a column, the image maps linearly onto the reference timeline.
| Property | Type | Default | Description |
|---|---|---|---|
mediaID |
string |
- | Identifies an image media entry. |
seekable? |
boolean |
false |
Lets the user click or drag the image to seek. |
seekMarginLeft? |
number |
0 |
Specifies the non-seekable left margin as a percentage. |
seekMarginRight? |
number |
0 |
Specifies the non-seekable right margin as a percentage. |
markerLayers? |
MarkerLayerConfig[] |
none | Specifies marker layers on the seek surface. |
css? |
object |
none | Overrides theming tokens for this view. |
perTrackImage
A perTrackImage view shows the imageID media entry of the active audio track. It supports players with exclusive solo.
Because that entry is a medium, it carries its own alignment column. The surface adopts the column of whichever track is soloed, so a per-stem spectrogram seeks and folds markers on its own time axis.
{
"type": "perTrackImage",
"seekable": true,
"markerLayers": [{ "set": "sections" }]
}
| Property | Type | Default | Description |
|---|---|---|---|
seekable? |
boolean |
false |
Lets the user click or drag the current track image to seek. |
seekMarginLeft? |
number |
0 |
Specifies the non-seekable left margin as a percentage. |
seekMarginRight? |
number |
0 |
Specifies the non-seekable right margin as a percentage. |
markerLayers? |
MarkerLayerConfig[] |
none | Specifies marker layers on the seek surface. |
css? |
object |
none | Overrides theming tokens for this view. |
waveform
A waveform view shows an interactive waveform. In a standard multitrack player, it can represent all audible tracks.
In an aligned player, a fixed-track waveform uses the local timeline of that track.
{
"type": "waveform",
"tracks": ["takeA"],
"height": 120,
"waveformBarWidth": 1,
"maxZoom": 5,
"playbackFollowMode": "center",
"timeAxis": "individual",
"timer": true,
"alignedPlayhead": true,
"markerLayers": [{ "set": "sections", "color": "#ed8c01" }]
}
| Property | Type | Default | Description |
|---|---|---|---|
tracks? |
"audible" \| string[] |
"audible" |
Selects the audio tracks for the waveform. |
height? |
number |
150 |
Specifies the waveform height in pixels. |
waveformBarWidth? |
number |
1 |
Specifies the thickness of waveform bars. |
maxZoom? |
number |
5 |
Specifies the smallest visible interval in seconds. A smaller value permits more zoom, and 0 lifts the zoom limit. |
playbackFollowMode? |
"off" \| "center" \| "jump" |
"center" |
Controls how the view moves with playback. |
timeAxis? |
"shared" \| "individual" |
"shared" ("individual" for tracks: "audible" under alignment) |
Selects a shared longest-track duration or the duration of each fixed track. |
timer? |
boolean |
false (true under alignment) |
Shows a local timer in the waveform. Each aligned waveform runs on its own timeline, so it carries a timer unless you set this to false. |
alignedPlayhead? |
boolean |
false |
Shows geometry from the reference playhead to the local playhead. |
markerLayers? |
MarkerLayerConfig[] |
none | Specifies marker layers on the waveform. |
css? |
object |
none | Overrides theming tokens for this view. |
If alignment exists, specify tracks. Its value must be "audible" or an array with one track ID.
If all audio uses one timeline, a fixed waveform can contain multiple tracks. Alignment also enables exclusive solo.
Thus, exactly one track is audible. The "audible" value follows the solo track.
The waveform shows the unwarped local shape, playhead, and markers of that track. It also supports alignedPlayhead and timeAxis: "individual".
For an aligned "audible" waveform, timeAxis defaults to "individual". This default shows the pace of the current track.
The current track can change with the solo state. Fixed sources use "shared" by default.
Set timeAxis explicitly to replace either default. This property changes the meaning of distances between playheads in different waveform views:
shared— Each waveform uses the duration of the longest track. A gap between playheads shows the absolute time difference between performances.individual— Each waveform uses its own total duration from 0% to 100%. A playhead shows progress through that recording.
In individual mode, different performance pacing produces different playhead positions for the same musical content. This difference is not a musical misalignment.
Only the start and end of the work always align. Neither mode warps waveform peaks to a shared reference axis.
Both modes show the natural recorded waveform of each track. In shared mode, shading covers the region after a shorter track ends.
The waveform and the zoom overview both show this shading.
midi
A midi view shows a MIDI file as a piano roll. MIDI files do not create audio output.
{
"type": "midi",
"mediaID": "notes",
"height": 180,
"maxZoom": 5,
"playbackFollowMode": "center",
"timer": true
}
| Property | Type | Default | Description |
|---|---|---|---|
mediaID |
string |
- | Identifies a media entry with type: "midi". |
height? |
number |
180 |
Specifies the piano-roll height in pixels. |
maxZoom? |
number |
5 |
Specifies the smallest visible interval in seconds, where 0 lifts the zoom limit. |
playbackFollowMode? |
"off" \| "center" \| "jump" |
"center" |
Controls how the MIDI view moves with playback. |
timer? |
boolean |
false |
Shows a local timer in the MIDI view. |
markerLayers? |
MarkerLayerConfig[] |
none | Specifies marker layers on the piano roll. |
css? |
object |
none | Overrides theming tokens for this view. |
mediaID must identify a media entry with type: "midi".
If alignment.timelines contains the same ID, the piano roll uses its local timeline for seeking, playback movement, loops, and markers.
sheetMusic
A sheetMusic view shows a MusicXML score.
{
"type": "sheetMusic",
"mediaID": "score",
"maxWidth": 1000,
"maxHeight": 370,
"renderScale": 0.7,
"followPlayback": true,
"cursorColor": "#999999",
"cursorAlpha": 0.4
}
| Property | Type | Default | Description |
|---|---|---|---|
mediaID |
string |
- | Identifies a media entry with type: "musicxml". |
maxWidth? |
number |
1000 |
Specifies the maximum score width in pixels. |
maxHeight? |
number |
380 |
Specifies the maximum score height in pixels. |
renderScale? |
number |
0.7 |
Specifies the scale for the score renderer. |
followPlayback? |
boolean |
true |
Moves the score view with playback. |
cursorColor? |
string |
"#999999" |
Specifies the playback cursor color. |
cursorAlpha? |
number |
0.4 |
Specifies playback cursor opacity from 0 to 1. |
css? |
object |
none | Overrides theming tokens for this view. |
mediaID must identify a media entry with type: "musicxml".
If alignment.timelines contains this ID, playback follow and measure seeking use the aligned timeline.
Measure numbers
A measures column refers to the measure numbers printed on the score, that is, the number attribute of each <measure> in the MusicXML.
An excerpt therefore keeps the numbering of the edition it comes from. If a score starts at bar 231, its alignment CSV starts at 231 as well, and no renumbering to 1 is needed.
The score timeline covers the range of printed numbers. Clicks and cursor positions report them.
warpingMatrix
A warpingMatrix view shows the relationship between two aligned timelines.
{
"type": "warpingMatrix",
"x": "takeA",
"y": "takeB",
"height": 220,
"tempoSmoothingSeconds": 5
}
| Property | Type | Default | Description |
|---|---|---|---|
x |
string |
- | Specifies the audio timeline for the horizontal axis. |
y |
string |
- | Specifies the audio timeline for the vertical axis. |
height? |
number |
auto | Specifies the chart height in pixels. |
tempoSmoothingSeconds? |
number |
none | Specifies the smoothing window for local tempo differences. |
css? |
object |
none | Overrides theming tokens for this view. |
x and y must identify timelines in alignment.timelines.
text
A text view adds a plain-text section.
{
"type": "text",
"text": "Compare the two performances",
"bold": true,
"italic": false,
"fontSize": 18,
"align": "center"
}
| Property | Type | Default | Description |
|---|---|---|---|
text |
string |
- | Specifies the text to show. |
bold? |
boolean |
false |
Shows the text in bold. |
italic? |
boolean |
false |
Shows the text in italic. |
fontSize? |
number |
inherited | Specifies the font size in pixels. |
align? |
"left" \| "center" \| "right" |
"center" |
Specifies the horizontal text alignment. |
css? |
object |
none | Overrides theming tokens for this view. |
The view treats the value as plain text, not HTML.
separator
A separator view draws a horizontal rule between panels.
{
"type": "separator",
"thickness": 4
}
| Property | Type | Default | Description |
|---|---|---|---|
thickness? |
number |
2 |
Specifies the rule thickness in pixels. |
The rule replaces the 1px hairline the player draws between panels, so a
separator never doubles up with it. It is not reorderable, even when
customizablePanelOrder is on.
trackList
A trackList view shows audio tracks and their controls.
{
"type": "trackList",
"tracks": ["takeA", "takeB"],
"soloGroup": 0,
"rowHeight": 52,
"trackVolumeControls": true,
"trackPanControls": "balance"
}
| Property | Type | Default | Description |
|---|---|---|---|
tracks |
string[] |
- | Specifies the audio media IDs in this list. |
title? |
string |
- | Labels the list. An aligned player shows it on the row that selects the list as a whole. |
soloGroup? |
number |
none | Names the selection this list belongs to, which permits only one active track at a time. Lists sharing a number share one selection. Use it to compare performances or stems. |
rowHeight? |
number |
auto | Fixes the track-row height in pixels. Padding, control size, icon size, font size and slider size scale down with it, so values below the default row height produce compact rows. |
trackVolumeControls? |
boolean |
false |
Shows a volume control for each track. |
trackPanControls? |
"balance" \| "pan" \| false |
false |
Shows a left-right pan control for each track and selects its algorithm. |
tracks contains audio media IDs. Every track in media must appear in some trackList view.
Multiple trackList views can show separate track groups in different layout positions.
soloGroup turns the list’s rows into radio buttons: activating one deactivates the
others, and the list always keeps one track active. Its value is any non-negative integer, and
it names the selection the list belongs to. A player can therefore combine a comparison group
with an ordinary mixer group — a list without a soloGroup mixes its tracks freely — and give
each comparison group a number of its own:
"views": [
{ "type": "trackList", "tracks": ["sopranoFlute", "sopranoOboe"], "soloGroup": 0 },
{ "type": "trackList", "tracks": ["altoFlute", "altoOboe"], "soloGroup": 1 }
]
Lists that name the same number share one selection: picking a track in either of them
deselects whatever the other had, so several lists act as one set of radio buttons. That is how
a player splits one comparison into separate lists — with a separator, a text caption, or
another view between them — without splitting the choice. Such a set opens with one audible
track between all of its lists.
An alignment block changes what the setting means. Alignment places each timeline at its own
audible position, so tracks of two timelines sounding together would sit at different moments
of the piece. The player therefore selects one timeline at a time, across all lists, and each
list decides what it contributes to that choice:
- With a
soloGroup— every row of the list is a selectable timeline of its own. - Without one — the list is one timeline, and its rows mix freely inside it. The list gets an extra row above its tracks that selects it as a whole, and its rows become a second level of the choice, where the last remaining active track stays active.
Because that selection already spans the whole player, an aligned player has at most one
soloGroup number to share; a second one would be the same selection under another name, and
the player rejects it.
Omitting soloGroup under alignment therefore claims that the list’s tracks are one
recording taken apart, such as separated stems. The player checks that claim: all of them must
name the same alignment.timelines column, declare the same timelineUnit, and carry the same
startOffsetMs and endOffsetMs. See the
Separated Stems use case.
Sync mode is what makes different timelines audible together: it runs the time-stretched
sources on a shared clock, and while it is on, all lists play simultaneously regardless of
soloGroup.
Each view controls the visibility of its volume and pan controls. trackPanControls also selects the pan algorithm:
"balance"(default) — Controls the gain of each stereo channel separately. At hard left, only the original left channel remains audible."pan"— Uses the equal-power pan law of the Web Audio APIStereoPannerNode.
For a stereo source, "pan" blends the channels as the value moves from the center. At hard left, both channels enter the left output.
This behavior is standard for mono content in a stereo field. A true stereo recording usually requires "balance".
navigationBar
A navigationBar view shows playback and navigation controls for the player.
{
"type": "navigationBar",
"controls": [
"playback",
"globalVolume",
"markerNavigation",
"looping",
"sync",
"presets",
"timer",
"seekBar"
],
"repeatEnabled": false
}
The player shows controls in the specified order. If synchronized sources are available, it shows sync.
If at least two presets exist, it shows presets.
| Property | Type | Default | Description |
|---|---|---|---|
controls |
TrackSwitchNavigationBarControl[] |
- | Specifies the required control list in order. See the supported values after this table. |
repeatEnabled? |
boolean |
false |
Enables repeat at player startup. |
controls supports "playback", "globalVolume", "markerNavigation", "looping", "sync", "presets", "timer", and "seekBar".
Features
features controls optional behavior for the complete player. Each view contains its own view-specific properties.
An omitted feature uses the default value in the table.
{
"features": {
"muteOtherPlayerInstances": true,
"customizablePanelOrder": false,
"tabView": false,
"keyboard": true
}
}
| Property | Type | Default | Description |
|---|---|---|---|
muteOtherPlayerInstances? |
boolean |
true |
When this player starts, it stops other TrackSwitch players on the page. |
customizablePanelOrder? |
boolean |
false |
Lets users change the order of visible view panels. It does not change track order. |
tabView? |
boolean |
false |
Shows track rows as tabs. |
keyboard? |
boolean |
true |
Enables keyboard shortcuts. |
The player rejects unknown feature keys.
Solo behavior is not a feature: each trackList view decides on its own whether only one of
its tracks may sound, and an alignment block makes the whole player resolve to a single
audible timeline. See soloGroup in the trackList section.
Theming
The player draws its colors and dimensions from CSS custom properties. A css block overrides
them. At the top level it applies to the whole player:
{
"css": {
"--ts-color-accent": "#00a0ff",
"--ts-track-min-height": "40px"
},
"media": { "…": "…" },
"views": ["…"]
}
Most views and every audio media entry accept the same block. Because custom properties inherit,
a block on a view reaches everything that view draws and nothing outside it — so an accent color
set on one waveform recolors that waveform alone:
{ "type": "waveform", "tracks": "audible", "css": { "--ts-color-accent": "#8c54ff" } }
A css block always wins over the stylesheet, including the responsive rules that adjust sizing
on narrow screens. The player rejects a token name it does not know, so a typo fails at load
rather than silently doing nothing.
Colors
| Token | Default |
|---|---|
--ts-color-text-primary |
#000000 |
--ts-color-text-inverse |
#ffffff |
--ts-color-text-muted |
#dddddd |
--ts-color-text-secondary |
#666666 |
--ts-color-text-error |
#c03328 |
--ts-color-surface-base |
#eeeeee |
--ts-color-surface-panel-dark |
#383838 |
--ts-color-surface-panel-dark-50 |
rgba(56, 56, 56, 0.5) |
--ts-color-surface-panel-dark-08 |
rgba(56, 56, 56, 0.08) |
--ts-color-surface-panel-dark-12 |
rgba(56, 56, 56, 0.12) |
--ts-color-surface-panel-dark-14 |
rgba(56, 56, 56, 0.14) |
--ts-color-surface-panel-dark-16 |
rgba(56, 56, 56, 0.16) |
--ts-color-surface-control |
#555555 |
--ts-color-surface-raised-16 |
rgba(255, 255, 255, 0.16) |
--ts-color-surface-raised-18 |
rgba(255, 255, 255, 0.18) |
--ts-color-surface-raised-42 |
rgba(255, 255, 255, 0.42) |
--ts-color-surface-raised-48 |
rgba(255, 255, 255, 0.48) |
--ts-color-surface-raised-52 |
rgba(255, 255, 255, 0.52) |
--ts-color-surface-raised-58 |
rgba(255, 255, 255, 0.58) |
--ts-color-surface-raised-62 |
rgba(255, 255, 255, 0.62) |
--ts-color-surface-raised-66 |
rgba(255, 255, 255, 0.66) |
--ts-color-surface-raised-72 |
rgba(255, 255, 255, 0.72) |
--ts-color-surface-raised-86 |
rgba(255, 255, 255, 0.86) |
--ts-color-surface-raised-88 |
rgba(255, 255, 255, 0.88) |
--ts-color-surface-raised-90 |
rgba(255, 255, 255, 0.9) |
--ts-color-surface-raised-92 |
rgba(255, 255, 255, 0.92) |
--ts-color-surface-raised-94 |
rgba(255, 255, 255, 0.94) |
--ts-color-surface-raised-98 |
rgba(255, 255, 255, 0.98) |
--ts-color-surface-inverse-soft-10 |
rgba(255, 255, 255, 0.1) |
--ts-color-surface-inverse-soft-12 |
rgba(255, 255, 255, 0.12) |
--ts-color-surface-inverse-soft-78 |
rgba(255, 255, 255, 0.78) |
--ts-color-border-mid |
#999999 |
--ts-color-border-mid-20 |
rgba(153, 153, 153, 0.2) |
--ts-color-border-mid-26 |
rgba(153, 153, 153, 0.26) |
--ts-color-border-mid-35 |
rgba(153, 153, 153, 0.35) |
--ts-color-border-mid-45 |
rgba(153, 153, 153, 0.45) |
--ts-color-border-mid-55 |
rgba(153, 153, 153, 0.55) |
--ts-color-border-mid-56 |
rgba(153, 153, 153, 0.56) |
--ts-color-border-mid-60 |
rgba(153, 153, 153, 0.6) |
--ts-color-border-mid-65 |
rgba(153, 153, 153, 0.65) |
--ts-color-border-mid-70 |
rgba(153, 153, 153, 0.7) |
--ts-color-border-mid-75 |
rgba(153, 153, 153, 0.75) |
--ts-color-border-hover-ring |
rgba(153, 153, 153, 0.18) |
--ts-color-accent |
#ed8c01 |
--ts-color-accent-soft-06 |
rgba(237, 140, 1, 0.06) |
--ts-color-accent-soft-08 |
rgba(237, 140, 1, 0.08) |
--ts-color-accent-soft-10 |
rgba(237, 140, 1, 0.1) |
--ts-color-accent-soft-15 |
rgba(237, 140, 1, 0.12) |
--ts-color-accent-soft-20 |
rgba(237, 140, 1, 0.2) |
--ts-color-accent-soft-30 |
rgba(237, 140, 1, 0.25) |
--ts-color-accent-soft-32 |
rgba(237, 140, 1, 0.32) |
--ts-color-accent-soft-45 |
rgba(237, 140, 1, 0.45) |
--ts-color-accent-soft-50 |
rgba(237, 140, 1, 0.5) |
--ts-color-accent-soft-55 |
rgba(237, 140, 1, 0.55) |
--ts-color-accent-shadow |
rgba(237, 140, 1, 0.18) |
--ts-color-overlay-dim |
rgba(0, 0, 0, 0.5) |
--ts-color-overlay-dim-68 |
rgba(0, 0, 0, 0.65) |
--ts-color-overlay-dim-48 |
rgba(0, 0, 0, 0.48) |
--ts-color-overlay-ring |
rgba(0, 0, 0, 0.18) |
--ts-color-overlay-button |
rgba(56, 56, 56, 0.82) |
--ts-color-shadow-soft |
var(--ts-color-overlay-ring) |
--ts-color-shadow-medium |
rgba(0, 0, 0, 0.22) |
--ts-color-shadow-strong |
rgba(0, 0, 0, 0.3) |
--ts-color-shadow-modal |
rgba(0, 0, 0, 0.26) |
--ts-color-shortcut-panel-bg |
rgba(72, 72, 72, 0.96) |
--ts-color-alignment-points |
rgba(128, 128, 128, 0.5) |
--ts-color-track-error-bg |
#dd9b9b |
--ts-color-track-error-soft |
rgba(192, 51, 40, 0.08) |
--ts-focus-ring-color |
var(--ts-color-accent-soft-45) |
Layout and sizing
| Token | Default |
|---|---|
--ts-main-control-icon-size |
14px |
--ts-main-control-large-button-size |
20px |
--ts-preset-selector-height |
24px |
--ts-main-control-height |
24px |
--ts-main-control-padding-block-start |
8px |
--ts-main-control-padding-block-end |
8px |
--ts-main-control-padding-inline |
12px |
--ts-main-control-row-gap |
8px |
--ts-main-control-column-gap |
16px |
--ts-main-control-playback-gap |
10px |
--ts-main-control-loop-gap |
2px |
--ts-track-min-height |
32px |
--ts-track-padding-block |
8px |
--ts-track-padding-inline |
10px |
--ts-track-control-column |
30px |
--ts-track-list-indent |
18px |
--ts-track-control-size |
24px |
--ts-track-mix-gap |
14px |
--ts-track-mix-padding |
14px |
--ts-seekbar-height |
6px |
--ts-main-seek-hit-height |
20px |
--ts-main-seekhead-width |
5px |
--ts-main-seekhead-half-width |
2.5px |
--ts-main-seekhead-height |
20px |
--ts-seek-marker-hit-width |
10px |
--ts-seek-marker-line-width |
2px |
--ts-seek-marker-dash |
6px |
--ts-seek-marker-gap |
2px |
--ts-waveform-seekhead-width |
2px |
--ts-panel-gutter-sm |
12px |
--ts-panel-gutter-md |
16px |
--ts-panel-gutter-lg |
22px |
--ts-interactive-panel-padding |
18px |
--ts-interactive-panel-gap |
18px |
--ts-slider-thumb-size |
12px |
--ts-slider-track-size |
5px |
--ts-slider-hit-size |
16px |
--ts-slider-width |
80px |
--ts-slider-track-color |
var(--ts-color-surface-control) |
--ts-slider-thumb-color |
var(--ts-color-accent) |
--ts-shortcut-delay |
40ms |
IDE Support
The player configuration has a published JSON Schema. Naming it in a config file gives editors that understand JSON Schema — VS Code and the JetBrains IDEs among them — completion, inline documentation and validation while writing:
{
"$schema": "https://audiolabs.github.io/trackswitch.js/schema/trackswitch.schema.json",
"media": { "…": "…" },
"views": ["…"]
}
Completion narrows as the config takes shape: once a view declares "type": "waveform", only the
waveform view’s options are offered, and a css block completes the theming tokens above with
their default values.
The player ignores $schema; it is only there for the editor.
This works for a config in its own file, loaded through the config-src attribute. An editor
cannot attach a schema to JSON embedded in an inline <script type="application/json"> element,
so a separate config file is the better choice while a player is being written.
Keyboard and loop controls
If features.keyboard is true, use these shortcuts:
| Keys | Action |
|---|---|
F1 |
Open or close the keyboard-shortcut panel. |
Space |
Play or pause. |
Escape |
Stop and return to the start. |
R |
Toggle repeat. |
Left / Right |
Move backward or forward by 2 seconds. |
Shift + Left / Shift + Right |
Move backward or forward by 5 seconds. |
Home |
Go to the start. |
Up / Down |
If controls contains "globalVolume", change the global volume. |
1 to 0 |
Control tracks 1 to 10. |
If controls contains "looping", use these additional shortcuts:
| Keys | Action |
|---|---|
A |
Set loop point A. |
B |
Set loop point B. |
L |
Turn the loop on or off. |
C |
Clear the loop. |
The loop buttons also control loops. On a seekable control, right-click to add a loop region.
If controls contains "markerNavigation", use these additional shortcuts:
| Keys | Action |
|---|---|
, |
Jump to the previous marker. |
. |
Jump to the next marker. |
Configuration requirements
mediamust contain at least one audio entry.viewsmust contain at least one view.viewscan contain a maximum of onenavigationBar.- Every ID referenced by
trackList,waveform.tracks, presets, or viewmediaIDmust exist inmedia. alignment.referenceTimelinemust be one of the keys inalignment.timelines.- Timeline IDs used by marker sets, MIDI views, and sheet music views must match IDs in
alignment.timelines. - Timeline IDs used by warping matrices must match IDs in
alignment.timelines, andxandymust differ. - Seekable
imageandperTrackImageviews needseekMarginLeft + seekMarginRightbelow100. - If one track is active at a time (a
trackListwith asoloGroup), useperTrackImage. warpingMatrixrequires analignmentblock.- Every track in
mediamust appear in sometrackListview. soloGroupmust be a non-negative integer.- With an
alignmentblock, atrackListwithout asoloGroupneeds all of its tracks on one timeline: the samealignment.timelinescolumn, the sametimelineUnit, and the samestartOffsetMsandendOffsetMs. Aligned players accept at most onesoloGroupnumber. perTrackImagerequires everytrackListview to declare the samesoloGroup.
The player rejects unknown keys and unknown values. A property that takes a fixed set of
values, such as playbackFollowMode or align, rejects anything outside that set instead of
falling back to its default, so a typo surfaces as an error rather than as unexpected behavior.
The same applies to numbers outside their valid range and to non-boolean values on boolean
properties.