Get Tone.js Transport Progress as Percentage or Beats

Tracking the playback position of the Tone.Transport is essential for building interactive audio applications, visualizers, or custom timeline progress bars in Tone.js. This article explains how to retrieve the current playback progress of the Tone.js transport both as a precise musical beat count and as a normalized percentage (0% to 100%), using built-in Tone.js helper classes and simple mathematical calculations.


Capturing Progress as a Beat Count

To get the exact current position as a decimal beat count, the most reliable method is to use the Tone.Time utility. While Tone.Transport.position returns a formatted string (such as "bars:beats:sixteenths"), passing this value into Tone.Time() allows you to convert it into a raw numerical beat format.

import * as Tone from 'tone';

function getCurrentBeats() {
  // Converts the current transport position string into a decimal beat count
  const position = Tone.Transport.position;
  const beatCount = Tone.Time(position).toBeats();
  return beatCount;
}

This method is highly accurate because it accounts for any changes in the transport’s tempo (BPM) or time signature dynamically.


Capturing Progress as a Percentage

To calculate playback progress as a percentage, you must compare the current transport time against a defined total duration or a loop end point.

If your application uses a looping transport, you can calculate the percentage by dividing the current elapsed seconds by the loopEnd value:

function getPlaybackPercentage() {
  const currentSeconds = Tone.Transport.seconds;
  const totalSeconds = Tone.Transport.loopEnd;

  if (totalSeconds === 0) return 0; // Prevent division by zero

  // Calculate percentage (0 to 100)
  const percentage = (currentSeconds / totalSeconds) * 100;
  
  // Clamp the value between 0 and 100
  return Math.min(Math.max(percentage, 0), 100);
}

If you are not using a loop but have a fixed song duration (for example, 16 bars), you can define that duration as a Tone.Time object and convert it to seconds for the calculation:

const songDurationSeconds = Tone.Time("16:0:0").toSeconds();

function getFixedPlaybackPercentage() {
  const currentSeconds = Tone.Transport.seconds;
  const percentage = (currentSeconds / songDurationSeconds) * 100;
  return Math.min(Math.max(percentage, 0), 100);
}

Implementing Real-Time Tracking

Because audio timing runs on a separate web audio thread, polling the transport inside a standard browser rendering loop is the best way to update your user interface. Using requestAnimationFrame allows you to query the transport properties continuously without degrading performance.

function updateUI() {
  const beats = getCurrentBeats().toFixed(2);
  const percentage = getPlaybackPercentage().toFixed(1);

  // Update DOM elements
  document.getElementById('beat-display').innerText = `Beats: ${beats}`;
  document.getElementById('progress-bar').style.width = `${percentage}%`;

  // Continue the loop if the transport is actively playing
  if (Tone.Transport.state === "started") {
    requestAnimationFrame(updateUI);
  }
}

// Start tracking when the transport starts
function playAudio() {
  Tone.Transport.start();
  requestAnimationFrame(updateUI);
}