Skip to content

Examples

The examples on this page make use of this MP4 file of a drum loop, with each part of the drum kit being in a separate track:

  • Bass drum (track ID 2)
  • Snare drum (track ID 3)
  • Hi-hat (track ID 4)

Controlling playback

Using HTMLMediaElement

If looking to simply play all the tracks contained in an MP4 file with minimal configuration, linking the player with an HTMLMediaElement is likely the easiest (albeit janky) way.

The multitrack player can remain in sync with the native media element UI by listening to the element's 'play', 'pause', 'seeked', and 'ratechange' events.

Basic setup

ts
const file: Blob = ...

const mediaElement: HTMLMediaElement = ...
mediaElement.src = URL.createObjectURL(file);
// Mute native player to let the multitrack player handle audio playback
mediaElement.muted = true;

const player = await ScheduledBuffersPlayer.fromBlob(file);
for (const track of Object.values(player.tracks)) {
  track.output.connect(player.context.destination);
}

mediaElement.onplay = () => {
  player.play();
};
mediaElement.onpause = () => {
  player.pause();
};
mediaElement.onseeked = () => {
  player.setPlaybackState({
    playbackPosition: mediaElement.currentTime,
    // `paused` must also be set in order to avoid issues when
    // a media element that is played to the end emits 'play'
    // before 'seeked' on replay
    paused: mediaElement.paused,
  });
};
mediaElement.onratechange = () => {
  player.setPlaybackState({
    playbackRate: mediaElement.playbackRate,
  });
};

Example

Try unmuting the media element to hear the first track (the track automatically selected by the browser) being played through both the native player and the multitrack player.

Using custom controls

Basic setup

ts
const file: Blob = ...

const player = await ScheduledBuffersPlayer.fromBlob(file);
for (const track of Object.values(player.tracks)) {
  track.output.connect(player.context.destination);
}

myPlayButton.onclick = () => {
  player.play();
};
myPauseButton.onclick = () => {
  player.pause();
};
myPlaybackPositionControl.onchange = () => {
  player.setPlaybackState({
    playbackPosition: myPlaybackPositionControl.value,
  });
};
myPlaybackRateControl.onchange = () => {
  player.setPlaybackState({
    playbackRate: myPlaybackRateControl.value,
  });
};

Example

Adjusting track offset

A track's offset (positive/negative delay) can be adjusted relative to the player's internal playback position, given the offset magnitude does not exceed config.maxOffset.

Basic setup with UI control

ts
const file: Blob = ...

const player = await ScheduledBuffersPlayer.fromBlob(file, {
  config: {
    maxOffset: 0.1, // max ±0.1 second offset
  },
});
for (const track of Object.values(player.tracks)) {
  track.output.connect(player.context.destination);
}

const track = player.tracks[3]; // track with ID of 3

myTrackOffsetControl.onchange = () => {
  track.setTrackState({
    offset: myTrackOffsetControl.value,
  });
};

Example

Linking with Web Audio API

By sharing the same AudioContext, you can connect your own intermediate audio nodes to the multitrack player's track outputs.

Basic setup

ts
const file: Blob = ...

const audioContext = new AudioContext();
const gainNode = new GainNode(audioContext); // same context as player
const targetTrackId = 2;

const player = await ScheduledBuffersPlayer.fromBlob(file, {
  context: audioContext, // same context as audio node
});
for (const track of Object.values(player.tracks)) {
  if (track.id === targetTrackId) {
    track.output
      .connect(gainNode)
      .connect(audioContext.destination);
  } else {
    track.output
      .connect(audioContext.destination);
  }
}

myGainControl.onchange = () => {
  gainNode.gain.value = myGainControl.value;
};

Example