trackswitch

Documentation

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>

Concepts

The configuration is built from four elementary blocks — media, markers, alignments, and views — which realize three concepts.

Timelines. A timeline is an ordered coordinate system for the temporal axis of one medium. Every entry in media defines its own timeline, identified by its media ID, and expressed in that medium’s unit: seconds for audio and MIDI, measures for a score, percent of width for an image. Media whose material occurs at exactly the same coordinates effectively share a timeline, so a player whose tracks are stems of one recording needs no alignment at all.

Markers. A marker is a discrete position on one timeline, optionally carrying a label. Markers that belong together — downbeats, measure starts, structural boundaries — form a marker sequence, configured as one entry in markers. A sequence is bound to the timeline its positions are read on, and one timeline may carry any number of sequences. Their order is what previous/next navigation steps along.

Alignments. An alignment relates timelines by pairing markers that refer to the same musical position. Each row of the alignment CSV is an anchor: one position per timeline column, all naming the same musical moment. Between consecutive anchors the player interpolates linearly, which turns the discrete anchors into a continuous projection between any two timelines. One timeline is designated the reference timeline, and mappings between the others are composed through it.

Two forms of navigation follow from this. Intra-timeline navigation steps between consecutive markers of a sequence — the next beat, the next section. Inter-timeline navigation projects the current position onto another timeline, so switching between performances, or clicking a measure in a score, keeps the musical position rather than the elapsed time.

Views. Views decide what is shown and in which order; they are the presentation layer over the data above, and are listed in Views.

Audio playback comes in two modes. In comparative listening one track sounds at a time and listeners switch between alternatives without interrupting playback — configured with comparisonGroup on a trackList. In simultaneous listening several tracks sound together, which requires them to share a timeline; media on distinct, aligned timelines can only be compared, unless time-scale-modified renditions are supplied through srcTimeScaled and the sync control is enabled.

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 the anchors relating two or more timelines.
markers no Contains named marker sequences, each read from a CSV file.
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.
volumeControl? boolean trackList’s trackVolumeControls Overrides the owning trackList’s volume control visibility for this track only.
panControl? "balance" \| "pan" \| "none" trackList’s trackPanControls Overrides the owning trackList’s pan control visibility (and algorithm) for this track only.
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.
srcTimeScaled? 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.

srcTimeScaled identifies a version of the recording that is already warped onto the reference timeline, which is what the sync control plays:

{
  "srcTimeScaled": { "src": "violin-synchronized.wav" }
}

srcTimeScaled 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,
  "srcTimeScaled": {
    "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",
    "duplicateAnchors": "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.
duplicateAnchors? "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 duplicateAnchors decides that case:

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.

A declared unit also sets what the zoom spans of a view are written in. maxZoom and defaultZoom on a pianoRoll view are read in the unit of the medium named by its mediaID; on a waveform view they are read in the unit of the reference timeline. Both fall back to seconds when no unit is declared. A unit that runs at a varying rate against seconds — MIDI ticks under a tempo change, or measures — converts the span at the start of the medium.

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:

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, and playback stops rather than continuing into that uncovered stretch.

markers

Marker sequences represent independent points or consecutive segments. Point sequences are for beats, onsets, lyrics, and other positions. Segment sequences are for sections, chords, and other annotations that remain valid until the next marker.

{
  "markers": {
    "sections": {
      "type": "segments",
      "src": "sections.csv",
      "timeline": "takeA",
      "timeCol": "start",
      "labelCol": "label"
    }
  }
}
Property Type Default Description
type "points" \| "segments" - Specifies whether each marker is an independent point or begins a segment that ends at the next marker.
colors? Record<string, string> none Maps segment labels to CSS colors. Requires type: "segments" and labelCol.
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.

The player reads timeCol values in the timeline unit of their timeline. The alignment column and the timeline display use the same unit. A sequence on a timeline with "timelineUnit": "samples" uses sample indices. A sequence on a musicxml timeline uses measure numbers. Without an alignment block, the sequence belongs to the single implicit timeline.

For type: "segments", each visible marker begins a half-open segment that ends at the next marker. The marker label belongs to that segment. The final visible marker extends to the hidden marker at the end of the timeline. Marker layers display these segments as labeled, translucent regions. The boundary markers remain available for seeking, navigation, and loop snapping. The optional colors map assigns a CSS color to each label. Labels without a map entry use the marker layer color.

Previous and next navigation uses the marker sequences a view currently shows. A sequence becomes a navigation target through a markerLayers entry, so a sequence 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 pianoRoll view, or an image keep their markers regardless of solo state.

The jump field searches all annotation sets. Visibility and audible track state do not affect these searches.

Each result identifies the marker sequence, 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.

Editing markers

The "markerEditing" navigation bar control lets the listener add, move, relabel, and remove markers by hand. Its button switches editing on and off. While editing is on, a tool group opens beside it with the sequence that new markers are added to, the add tool, the remove tool, a click toggle, and a CSV download.

A click on a marker still jumps to it, and a right-click drag still sets a loop. Boundaries of a segments sequence cannot be moved past their neighbours.

A marker CSV may consist of a header row only, which gives an empty sequence to fill by hand. The download writes the sequence in the column layout of its CSV, with positions in the unit of its timeline. Untouched rows keep their original values, including columns the player does not read. After an edit, the markers of a sequence are numbered again in timeline order.

The same edits are available from code: addMarker, updateMarker, removeMarker, and getMarkersCsv on the controller, and the markers event (trackswitch-markers on the element), which reports the ID of the edited sequence.

Views show marker sequences through markerLayers:

{
  "type": "waveform",
  "tracks": ["takeA"],
  "markerLayers": [
    { "sequence": "sections", "color": "#ed8c01", "line": "dashed", "lineWidth": 2 },
    { "sequence": "alignment", "color": "#777", "foldToReference": true }
  ]
}

sequence identifies a marker sequence. The special alignment sequence 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
sequence string - Specifies a marker sequence ID or the implicit alignment anchors.
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 comparisonGroup 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 pianoRoll 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": [{ "sequence": "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": [{ "sequence": "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,
  "defaultZoom": 30,
  "playbackFollowMode": "center",
  "timeAxis": "individual",
  "timer": true,
  "alignedPlayhead": true,
  "markerLayers": [{ "sequence": "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. A smaller value permits more zoom, and 0 lifts the zoom limit.
defaultZoom? number none Specifies the visible interval the view opens on. Unset shows the whole timeline.
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:

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.

pianoRoll

A pianoRoll view shows a MIDI file as a piano roll. MIDI files do not create audio output.

{
  "type": "pianoRoll",
  "mediaID": "notes",
  "height": 180,
  "maxZoom": 5,
  "defaultZoom": 30,
  "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, where 0 lifts the zoom limit.
defaultZoom? number none (10 seconds with pianoKeyboard) Specifies the visible interval the view opens on. Unset shows the whole file.
playbackFollowMode? "off" \| "center" \| "jump" "center" Controls how the piano roll moves with playback. Ignored while pianoKeyboard is on — see there.
timer? boolean false Shows a local timer in the piano roll.
pianoKeyboard? boolean false Draws a piano keyboard beside the pitch axis and pins the playhead to its edge. Forces a pinned-left follow that overrides playbackFollowMode, and changes the note grid’s drag gesture to pan the sheet under the pointer instead of jumping to the click.
noteRange? "automatic" \| [note, note] "automatic" Fixes the pitch axis. Each entry is a note name or a MIDI note number.
grid? "none" \| "time" \| "pitch" \| "both" "none" Draws reference lines behind the notes: a time grid, a pitch grid, or both.
noteTooltip? boolean false Shows the pitch, channel, start, end, duration and velocity of the note event under the cursor.
velocityBars? boolean false Draws a bar inside each note event showing its velocity.
velocityOpacity? boolean false Fades note events by their velocity instead of drawing them solid.
channelToTrackIDMap? object none Pairs MIDI channels with audio tracks, keyed by channel number.
colorPerChannel? boolean true Gives every channel in the file its own palette colour.
palette? "light" \| "dark" \| "colorblind-light" \| "colorblind-dark" "light" Selects the channel colour set and background.
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.

Grid and note readout

A time grid draws its lines on round values of the unit the medium declares in timelineUnit, so a MIDI entry declaring "ticks" gets a grid on beats rather than on seconds — one that stays with the music through a tempo change. The interval steps up and down as the view is zoomed, keeping the lines readably far apart.

A pitch grid bands the rows of the black keys, which is the same reading the pianoKeyboard column gives — dark rows for the black keys against light ones for the white keys, in every palette. It needs a few pixels per semitone, so a short roll spanning a wide noteRange draws no pitch grid.

noteTooltip reads out the note event under the cursor — its pitch, channel, start, end, duration and velocity — with the times in the same unit as the view’s timer and the velocity as the 0–127 value the file was written with.

Channels

A file that transcribes several instruments carries one channel per instrument. With colorPerChannel at its default of true, every channel already takes its own colour — a single combined recording gets a legible piano roll with no further configuration.

channelToTrackIDMap additionally pairs channels with the audio tracks of the player, so a channel is drawn only while one of its tracks is audible:

{
  "type": "pianoRoll",
  "mediaID": "notes",
  "channelToTrackIDMap": { "0": "soprano", "1": "alto", "2": "tenor", "3": "bass" }
}

Each key is a channel number from 0 to 15, and each value names a media entry with type: "audio", or an array of several. Two channels may name the same track — the track’s row then splits its colour across both, with a hard edge, instead of picking just one.

A value can also be a list of tracks, which keeps the channel visible while any one of them is audible. This suits a channel that belongs to a comparisonGroup of alternate takes — one mixed recording, say, standing in for four solo tracks that are never audible at the same time:

{
  "type": "pianoRoll",
  "mediaID": "notes",
  "channelToTrackIDMap": {
    "0": ["soprano", "mix"],
    "1": ["alto", "mix"],
    "2": ["tenor", "mix"],
    "3": ["bass", "mix"]
  }
}

A paired channel is drawn only while one of its tracks is audible, following the same rule as a waveform with "tracks": "audible". A channel the map leaves out is always drawn, still in its own colour by default. Set colorPerChannel to false to turn off per-channel colour entirely — every channel then falls back to the plain, unpaired colour, whether or not it’s in the map.

Coloured channels take the colours --ts-color-channel-1 to --ts-color-channel-16 by ascending channel number — one per MIDI channel, so a 16-channel file never repeats a colour. The first colour is the accent, so a file with a single channel looks like it did before this view had a palette at all.

Palette

palette picks the channel colour set and the surface behind it:

{
  "type": "pianoRoll",
  "mediaID": "notes",
  "palette": "dark"
}

Any of these can still be overridden per view with css, for an individual channel or the background. A channel’s fill and border are separate tokens from its solid colour, so recolouring one fully means setting all three (with the matching -dark/-colorblind/-colorblind-dark suffix):

{
  "type": "pianoRoll",
  "mediaID": "notes",
  "palette": "dark",
  "css": {
    "--ts-color-channel-3-dark": "#00ffaa",
    "--ts-color-channel-3-dark-soft-30": "rgba(0, 255, 170, 0.25)",
    "--ts-color-channel-3-dark-soft-55": "rgba(0, 255, 170, 0.55)"
  }
}

The background is --ts-color-surface-base (light) or --ts-color-surface-base-dark (dark).

Pitch axis

With noteRange at its default of "automatic", the pitch axis spans every note of the file, hidden channels included, so switching a track off never rescales the roll. A pair fixes it instead. Each entry is a MIDI note number or a name in scientific pitch notation, where middle C is C4 = 60, so these two are the same axis:

{ "type": "pianoRoll", "mediaID": "notes", "noteRange": ["C1", "C4"] }
{ "type": "pianoRoll", "mediaID": "notes", "noteRange": [24, 60] }

Piano keyboard

pianoKeyboard draws a keyboard column beside the pitch axis and turns the roll into a falling-notes display. Its far edge is a real keyboard — seven white keys per octave, with the black keys stopping short of it — while its near edge carries one row per semitone, the grid the roll itself draws on, so a note bar meets its own key. Every C carries a gray label.

A sounding note lights its key in the colour of its channel. Two channels holding one pitch split that key along its length, one box each.

The keyboard changes how the roll follows playback and how its note grid drags, on top of a defaultZoom default that can be set on its own:

Velocity

Note events are drawn solid. Velocity is opt-in, through two switches that work independently: velocityBars draws a small bar inside each note event, and velocityOpacity fades the note by its velocity.

Because solid notes cannot be told apart by transparency, a pitch several channels sound at the same time is drawn as a checkerboard over the stretch they share: one row per channel, with the colours rotating by one row from column to column.

A second note-on for a pitch already sounding on the same channel is read as a re-trigger — the note that was running ends there, instead of the two overlapping on one row.

See Piano Roll for a complete player.

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"],
  "comparisonGroup": 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.
comparisonGroup? 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" \| "none" "none" Shows a left-right pan control for each track and selects its algorithm.
channelColorIcons? boolean true Repeats a track’s piano-roll channel colour(s) on its solo icon. Set to false to keep every row’s icon in the plain foreground colour regardless of channel pairing.

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.

comparisonGroup 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 comparisonGroup mixes its tracks freely — and give each comparison group a number of its own:

"views": [
  { "type": "trackList", "tracks": ["sopranoFlute", "sopranoOboe"], "comparisonGroup": 0 },
  { "type": "trackList", "tracks": ["altoFlute", "altoOboe"], "comparisonGroup": 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:

Because that selection already spans the whole player, an aligned player has at most one comparisonGroup number to share; a second one would be the same selection under another name, and the player rejects it.

Omitting comparisonGroup 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 comparisonGroup.

A track’s pan control snaps to dead center (50/50) and only moves freely once dragged past a 35/65 split, so a near-center release always lands exactly on 50/50.

Each view controls the visibility of its volume and pan controls. trackPanControls also selects the pan algorithm:

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".

A track’s own volumeControl and panControl (on its media entry) override these per track, so a single trackList can show mix controls on some tracks and not others without splitting into multiple lists.

A navigationBar view shows playback and navigation controls for the player.

{
  "type": "navigationBar",
  "controls": [
    "playback",
    "globalVolume",
    "globalPan",
    "markerNavigation",
    "looping",
    "sync",
    "presets",
    "timer",
    "seekBar"
  ],
  "repeatEnabled": false,
  "globalPanControl": "balance"
}

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.
globalPanControl? "balance" \| "pan" "balance" Selects the pan algorithm for the global pan control shown by "globalPan". See trackPanControls for the algorithm behaviors.

controls supports "playback", "globalVolume", "globalPan", "markerNavigation", "markerEditing", "looping", "sync", "presets", "timer", and "seekBar".

"globalPan" shows a left-right pan control that adjusts every track together, styled like a track’s own pan control. It snaps to dead center the same way per-track pan controls do.

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,
    "normalizeLoudness": false,
    "autoload": false
  }
}
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.
normalizeLoudness? boolean false Normalizes each track to -14 LUFS integrated loudness at load time, clamped so it never clips.
autoload? boolean false Decodes the audio and renders the waveforms as soon as the player is created, instead of waiting for the first click or keypress. This doesn’t start playback — browsers only gate producing sound behind a user gesture, not decoding — so it needs no interaction, but it does spend bandwidth and CPU on every visitor whether or not they press play.

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 comparisonGroup 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-font-ui system-ui, sans-serif
--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-channel-1 var(--ts-color-accent)
--ts-color-channel-1-soft-30 var(--ts-color-accent-soft-30)
--ts-color-channel-1-soft-55 var(--ts-color-accent-soft-55)
--ts-color-channel-2 #c03328
--ts-color-channel-2-soft-30 rgba(192, 51, 40, 0.25)
--ts-color-channel-2-soft-55 rgba(192, 51, 40, 0.55)
--ts-color-channel-3 #3d8b37
--ts-color-channel-3-soft-30 rgba(61, 139, 55, 0.25)
--ts-color-channel-3-soft-55 rgba(61, 139, 55, 0.55)
--ts-color-channel-4 #2f6fb0
--ts-color-channel-4-soft-30 rgba(47, 111, 176, 0.25)
--ts-color-channel-4-soft-55 rgba(47, 111, 176, 0.55)
--ts-color-channel-5 #9467bd
--ts-color-channel-5-soft-30 rgba(148, 103, 189, 0.25)
--ts-color-channel-5-soft-55 rgba(148, 103, 189, 0.55)
--ts-color-channel-6 #8c564b
--ts-color-channel-6-soft-30 rgba(140, 86, 75, 0.25)
--ts-color-channel-6-soft-55 rgba(140, 86, 75, 0.55)
--ts-color-channel-7 #e377c2
--ts-color-channel-7-soft-30 rgba(227, 119, 194, 0.25)
--ts-color-channel-7-soft-55 rgba(227, 119, 194, 0.55)
--ts-color-channel-8 #7f7f7f
--ts-color-channel-8-soft-30 rgba(127, 127, 127, 0.25)
--ts-color-channel-8-soft-55 rgba(127, 127, 127, 0.55)
--ts-color-channel-9 #bcbd22
--ts-color-channel-9-soft-30 rgba(188, 189, 34, 0.25)
--ts-color-channel-9-soft-55 rgba(188, 189, 34, 0.55)
--ts-color-channel-10 #17becf
--ts-color-channel-10-soft-30 rgba(23, 190, 207, 0.25)
--ts-color-channel-10-soft-55 rgba(23, 190, 207, 0.55)
--ts-color-channel-11 #669e2e
--ts-color-channel-11-soft-30 rgba(102, 158, 46, 0.25)
--ts-color-channel-11-soft-55 rgba(102, 158, 46, 0.55)
--ts-color-channel-12 #2e9e5d
--ts-color-channel-12-soft-30 rgba(46, 158, 93, 0.25)
--ts-color-channel-12-soft-55 rgba(46, 158, 93, 0.55)
--ts-color-channel-13 #2e379e
--ts-color-channel-13-soft-30 rgba(46, 55, 158, 0.25)
--ts-color-channel-13-soft-55 rgba(46, 55, 158, 0.55)
--ts-color-channel-14 #6f2e9e
--ts-color-channel-14-soft-30 rgba(111, 46, 158, 0.25)
--ts-color-channel-14-soft-55 rgba(111, 46, 158, 0.55)
--ts-color-channel-15 #9e2e9e
--ts-color-channel-15-soft-30 rgba(158, 46, 158, 0.25)
--ts-color-channel-15-soft-55 rgba(158, 46, 158, 0.55)
--ts-color-channel-16 #9e2e50
--ts-color-channel-16-soft-30 rgba(158, 46, 80, 0.25)
--ts-color-channel-16-soft-55 rgba(158, 46, 80, 0.55)
--ts-color-surface-base-dark #2e2e2e
--ts-color-channel-1-dark #e6a14c
--ts-color-channel-1-dark-soft-30 rgba(230, 161, 76, 0.25)
--ts-color-channel-1-dark-soft-55 rgba(230, 161, 76, 0.55)
--ts-color-channel-2-dark #e6594c
--ts-color-channel-2-dark-soft-30 rgba(230, 89, 76, 0.25)
--ts-color-channel-2-dark-soft-55 rgba(230, 89, 76, 0.55)
--ts-color-channel-3-dark #4ce654
--ts-color-channel-3-dark-soft-30 rgba(76, 230, 84, 0.25)
--ts-color-channel-3-dark-soft-55 rgba(76, 230, 84, 0.55)
--ts-color-channel-4-dark #4c9ce6
--ts-color-channel-4-dark-soft-30 rgba(76, 156, 230, 0.25)
--ts-color-channel-4-dark-soft-55 rgba(76, 156, 230, 0.55)
--ts-color-channel-5-dark #854ce6
--ts-color-channel-5-dark-soft-30 rgba(133, 76, 230, 0.25)
--ts-color-channel-5-dark-soft-55 rgba(133, 76, 230, 0.55)
--ts-color-channel-6-dark #e6634c
--ts-color-channel-6-dark-soft-30 rgba(230, 99, 76, 0.25)
--ts-color-channel-6-dark-soft-55 rgba(230, 99, 76, 0.55)
--ts-color-channel-7-dark #e64cb8
--ts-color-channel-7-dark-soft-30 rgba(230, 76, 184, 0.25)
--ts-color-channel-7-dark-soft-55 rgba(230, 76, 184, 0.55)
--ts-color-channel-8-dark #b3b3b3
--ts-color-channel-8-dark-soft-30 rgba(179, 179, 179, 0.25)
--ts-color-channel-8-dark-soft-55 rgba(179, 179, 179, 0.55)
--ts-color-channel-9-dark #e3e64c
--ts-color-channel-9-dark-soft-30 rgba(227, 230, 76, 0.25)
--ts-color-channel-9-dark-soft-55 rgba(227, 230, 76, 0.55)
--ts-color-channel-10-dark #4ccfe6
--ts-color-channel-10-dark-soft-30 rgba(76, 207, 230, 0.25)
--ts-color-channel-10-dark-soft-55 rgba(76, 207, 230, 0.55)
--ts-color-channel-11-dark #99e64c
--ts-color-channel-11-dark-soft-30 rgba(153, 230, 76, 0.25)
--ts-color-channel-11-dark-soft-55 rgba(153, 230, 76, 0.55)
--ts-color-channel-12-dark #4ce68c
--ts-color-channel-12-dark-soft-30 rgba(76, 230, 140, 0.25)
--ts-color-channel-12-dark-soft-55 rgba(76, 230, 140, 0.55)
--ts-color-channel-13-dark #4c59e6
--ts-color-channel-13-dark-soft-30 rgba(76, 89, 230, 0.25)
--ts-color-channel-13-dark-soft-55 rgba(76, 89, 230, 0.55)
--ts-color-channel-14-dark #a64ce6
--ts-color-channel-14-dark-soft-30 rgba(166, 76, 230, 0.25)
--ts-color-channel-14-dark-soft-55 rgba(166, 76, 230, 0.55)
--ts-color-channel-15-dark #e64ce6
--ts-color-channel-15-dark-soft-30 rgba(230, 76, 230, 0.25)
--ts-color-channel-15-dark-soft-55 rgba(230, 76, 230, 0.55)
--ts-color-channel-16-dark #e64c7a
--ts-color-channel-16-dark-soft-30 rgba(230, 76, 122, 0.25)
--ts-color-channel-16-dark-soft-55 rgba(230, 76, 122, 0.55)
--ts-color-channel-1-colorblind var(--ts-color-accent)
--ts-color-channel-1-colorblind-soft-30 var(--ts-color-accent-soft-30)
--ts-color-channel-1-colorblind-soft-55 var(--ts-color-accent-soft-55)
--ts-color-channel-2-colorblind #56b4e9
--ts-color-channel-2-colorblind-soft-30 rgba(86, 180, 233, 0.25)
--ts-color-channel-2-colorblind-soft-55 rgba(86, 180, 233, 0.55)
--ts-color-channel-3-colorblind #009e73
--ts-color-channel-3-colorblind-soft-30 rgba(0, 158, 115, 0.25)
--ts-color-channel-3-colorblind-soft-55 rgba(0, 158, 115, 0.55)
--ts-color-channel-4-colorblind #b8a100
--ts-color-channel-4-colorblind-soft-30 rgba(184, 161, 0, 0.25)
--ts-color-channel-4-colorblind-soft-55 rgba(184, 161, 0, 0.55)
--ts-color-channel-5-colorblind #0072b2
--ts-color-channel-5-colorblind-soft-30 rgba(0, 114, 178, 0.25)
--ts-color-channel-5-colorblind-soft-55 rgba(0, 114, 178, 0.55)
--ts-color-channel-6-colorblind #d55e00
--ts-color-channel-6-colorblind-soft-30 rgba(213, 94, 0, 0.25)
--ts-color-channel-6-colorblind-soft-55 rgba(213, 94, 0, 0.55)
--ts-color-channel-7-colorblind #cc79a7
--ts-color-channel-7-colorblind-soft-30 rgba(204, 121, 167, 0.25)
--ts-color-channel-7-colorblind-soft-55 rgba(204, 121, 167, 0.55)
--ts-color-channel-8-colorblind #7f7f7f
--ts-color-channel-8-colorblind-soft-30 rgba(127, 127, 127, 0.25)
--ts-color-channel-8-colorblind-soft-55 rgba(127, 127, 127, 0.55)
--ts-color-channel-9-colorblind #f5bc6c
--ts-color-channel-9-colorblind-soft-30 rgba(245, 188, 108, 0.25)
--ts-color-channel-9-colorblind-soft-55 rgba(245, 188, 108, 0.55)
--ts-color-channel-10-colorblind #9dd4f2
--ts-color-channel-10-colorblind-soft-30 rgba(157, 212, 242, 0.25)
--ts-color-channel-10-colorblind-soft-55 rgba(157, 212, 242, 0.55)
--ts-color-channel-11-colorblind #6bc7ae
--ts-color-channel-11-colorblind-soft-30 rgba(107, 199, 174, 0.25)
--ts-color-channel-11-colorblind-soft-55 rgba(107, 199, 174, 0.55)
--ts-color-channel-12-colorblind #d6c86b
--ts-color-channel-12-colorblind-soft-30 rgba(214, 200, 107, 0.25)
--ts-color-channel-12-colorblind-soft-55 rgba(214, 200, 107, 0.55)
--ts-color-channel-13-colorblind #6badd2
--ts-color-channel-13-colorblind-soft-30 rgba(107, 173, 210, 0.25)
--ts-color-channel-13-colorblind-soft-55 rgba(107, 173, 210, 0.55)
--ts-color-channel-14-colorblind #e7a26b
--ts-color-channel-14-colorblind-soft-30 rgba(231, 162, 107, 0.25)
--ts-color-channel-14-colorblind-soft-55 rgba(231, 162, 107, 0.55)
--ts-color-channel-15-colorblind #e1b1cc
--ts-color-channel-15-colorblind-soft-30 rgba(225, 177, 204, 0.25)
--ts-color-channel-15-colorblind-soft-55 rgba(225, 177, 204, 0.55)
--ts-color-channel-16-colorblind #b5b5b5
--ts-color-channel-16-colorblind-soft-30 rgba(181, 181, 181, 0.25)
--ts-color-channel-16-colorblind-soft-55 rgba(181, 181, 181, 0.55)
--ts-color-channel-1-colorblind-dark var(--ts-color-channel-1-dark)
--ts-color-channel-1-colorblind-dark-soft-30 var( --ts-color-channel-1-dark-soft-30 )
--ts-color-channel-1-colorblind-dark-soft-55 var( --ts-color-channel-1-dark-soft-55 )
--ts-color-channel-2-colorblind-dark #7bc5ee
--ts-color-channel-2-colorblind-dark-soft-30 rgba(123, 197, 238, 0.25)
--ts-color-channel-2-colorblind-dark-soft-55 rgba(123, 197, 238, 0.55)
--ts-color-channel-3-colorblind-dark #38b392
--ts-color-channel-3-colorblind-dark-soft-30 rgba(56, 179, 146, 0.25)
--ts-color-channel-3-colorblind-dark-soft-55 rgba(56, 179, 146, 0.55)
--ts-color-channel-4-colorblind-dark #c8b638
--ts-color-channel-4-colorblind-dark-soft-30 rgba(200, 182, 56, 0.25)
--ts-color-channel-4-colorblind-dark-soft-55 rgba(200, 182, 56, 0.55)
--ts-color-channel-5-colorblind-dark #3891c3
--ts-color-channel-5-colorblind-dark-soft-30 rgba(56, 145, 195, 0.25)
--ts-color-channel-5-colorblind-dark-soft-55 rgba(56, 145, 195, 0.55)
--ts-color-channel-6-colorblind-dark #de8138
--ts-color-channel-6-colorblind-dark-soft-30 rgba(222, 129, 56, 0.25)
--ts-color-channel-6-colorblind-dark-soft-55 rgba(222, 129, 56, 0.55)
--ts-color-channel-7-colorblind-dark #d796ba
--ts-color-channel-7-colorblind-dark-soft-30 rgba(215, 150, 186, 0.25)
--ts-color-channel-7-colorblind-dark-soft-55 rgba(215, 150, 186, 0.55)
--ts-color-channel-8-colorblind-dark #9b9b9b
--ts-color-channel-8-colorblind-dark-soft-30 rgba(155, 155, 155, 0.25)
--ts-color-channel-8-colorblind-dark-soft-55 rgba(155, 155, 155, 0.55)
--ts-color-channel-9-colorblind-dark #f7cb8d
--ts-color-channel-9-colorblind-dark-soft-30 rgba(247, 203, 141, 0.25)
--ts-color-channel-9-colorblind-dark-soft-55 rgba(247, 203, 141, 0.55)
--ts-color-channel-10-colorblind-dark #b3ddf5
--ts-color-channel-10-colorblind-dark-soft-30 rgba(179, 221, 245, 0.25)
--ts-color-channel-10-colorblind-dark-soft-55 rgba(179, 221, 245, 0.55)
--ts-color-channel-11-colorblind-dark #8cd3c0
--ts-color-channel-11-colorblind-dark-soft-30 rgba(140, 211, 192, 0.25)
--ts-color-channel-11-colorblind-dark-soft-55 rgba(140, 211, 192, 0.55)
--ts-color-channel-12-colorblind-dark #dfd58c
--ts-color-channel-12-colorblind-dark-soft-30 rgba(223, 213, 140, 0.25)
--ts-color-channel-12-colorblind-dark-soft-55 rgba(223, 213, 140, 0.55)
--ts-color-channel-13-colorblind-dark #8cc0dc
--ts-color-channel-13-colorblind-dark-soft-30 rgba(140, 192, 220, 0.25)
--ts-color-channel-13-colorblind-dark-soft-55 rgba(140, 192, 220, 0.55)
--ts-color-channel-14-colorblind-dark #ecb78c
--ts-color-channel-14-colorblind-dark-soft-30 rgba(236, 183, 140, 0.25)
--ts-color-channel-14-colorblind-dark-soft-55 rgba(236, 183, 140, 0.55)
--ts-color-channel-15-colorblind-dark #e8c3d7
--ts-color-channel-15-colorblind-dark-soft-30 rgba(232, 195, 215, 0.25)
--ts-color-channel-15-colorblind-dark-soft-55 rgba(232, 195, 215, 0.55)
--ts-color-channel-16-colorblind-dark #c5c5c5
--ts-color-channel-16-colorblind-dark-soft-30 rgba(197, 197, 197, 0.25)
--ts-color-channel-16-colorblind-dark-soft-55 rgba(197, 197, 197, 0.55)
--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-piano-roll-keyboard-width 64px
--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.

These shortcuts step through the markers and are always available. The start and the end of the timeline count as stops, so they also work without any marker sequence:

Keys Action
, Jump to the previous marker, or to the start.
. Jump to the next marker, or to the end.

If controls contains "markerEditing", these shortcuts apply while marker editing is on:

Keys Action
M Add a marker at the current position.
Delete Remove the focused marker.
Ctrl + Z Undo the last marker edit.

Configuration requirements

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.