bbc / peaks.js

JavaScript UI component for interacting with audio waveforms
https://waveform.prototyping.bbc.co.uk
GNU Lesser General Public License v3.0
3.16k stars 277 forks source link

Using setSource with multiple audio formats #434

Closed benjaminhouy closed 2 years ago

benjaminhouy commented 2 years ago

Hi,

Forgive me if this is a silly question, I'm very new to programming.

I would like to serve my audio files in both MP3 and Webm format so I have the following audio tag:

<audio preload="auto">
  <source src="<%= @normal_audio_webm %>" type="audio/webm">
  <source src="<%= @normal_audio_mp3 %>" type="audio/mpeg">
</audio>

But I also want people to be able to switch to another audio file so I have this function that's triggered when users click on a button:

function loadPractice() {
  const options = {
    mediaUrl: '<%= @practice_audio_mp3 %>',
    dataUri: {
      arraybuffer: '<%= @practice_waveform %>' 
    }
  };
  instance.setSource(options, function(error) {
    const view = instance.views.getView('overview');
    view.setWaveformColor('red');
  });
  instance.player.play();
  explanation.textContent = "Try to guess the French translation of the phrase you hear. Then try to imitate the native speaker's pronunciation."
};

This works and: '<%= @practice_audio_mp3 %>' loads the corresponding MP3 files but I'm not sure how to add the webm file as well when using setSource. Is it something that's possible?

In other words, can I load several versions of the same file in different audio formats with setSource?

Thanks

chrisn commented 2 years ago

This is a good question. Currently setSource only allows you to provide a single URL, and it sets the <audio> element's src attribute.

I note that the HTML spec recommends not to update the <audio> element's <source> elements:

Dynamically modifying a source element and its attribute when the element is already inserted in a video or audio element will have no effect. To change what is playing, just use the src attribute on the media element directly, possibly making use of the canPlayType() method to pick from amongst available resources. Generally, manipulating source elements manually after the document has been parsed is an unnecessarily complicated approach.

Following the above advice, I'd recommend using canPlayType to decide which format audio to use.

const audioElement = document.getElementById('audio');
const result = audio.canPlayType('audio/webm; codecs="opus"');
console.log(result); // "probably" or "maybe" or ""

Note that for webm audio, you should pass a codecs value to tell the browser which audio encoding you're using.

How about something like this, which selects the first audio source which can "probably" be played or otherwise the first that can "maybe" be played?

const audioSources = [
  { src: '<%= @audio_webm_url %>', type: 'audio/webm; codecs="opus"' },
  { src: '<%= @audio_mp3_url %>', type: 'audio/mpeg' }
];

function getAudioSource(audioSources, audioElement) {
  const results = ['probably', 'maybe'];

  for (const result of results) {
    for (const source of audioSources) {
      if (audioElement.canPlayType(source.type) === result) {
        return source.src;
      }
    }
  }

  return null; // No playable audio URL
}

Another option is to re-render the <audio> element with new <source> child elements, and use Peaks.init() instead of setSource.

benjaminhouy commented 2 years ago

Hi @chrisn

Thanks for your very helpful reply :). This solved my issue and it's now working.

chrisn commented 2 years ago

Great, thank you for letting me know. I'll go ahead and close this issue.