9 min read

Wavesurfing

Over the past two years I’ve been leaning towards simply using a native HTML5 audio player rather than embedding everything via SoundCloud on my site. It feels easier, lighter. It’s quicker to load and there aren’t any cookie notices to tug at you.

SoundCloud, Goodbye

I’ve come to realise that it’s not always that one needs to see the the form of the audio. A long stream of podcast speech, for example, doesn’t typically benefit much from being presented with a waveform.1 A simple native player2 is less distracting and makes the file easier to huffduff or download. It doesn’t look the same in all browsers, but I don’t find that an issue.

When it comes to musical compositions there are however many cases in which it is handy to have an overview of the form of a piece. Jeremy Keith has also written about the here and now aspect that SoundCloud is so well suited to, as opposed to the time-shifted nature of podcasts. My main reason for moving away from (SoundCloud) embeds though, has been the desire to make sure that the core content of my website is hosted on the site itself.3 Nonetheless, it was SoundCloud’s rude decision to slap a big orange banner across their mobile embeds that pushed me to get on with finding a self-hosted alternative – one that would ideally also have native audio fallback should JavaScript be unavailable.

Wavesurfer, Hello

My searches led me to Wavesufer.js, a JavaScript player with a waveform overview that can be tailored to look something like SoundCloud’s, should one so wish. There are also plugins for adding regions, annotations, timelines, and even a spectrogram. Setting up the various options is demonstrated on their site, and I quickly had some examples up and running. I did however run into a few issues, and since not everything is immediately obvious from the demos provided, I thought it might be worthwhile to collect a few notes from my journey through implementing the player on my site.

Load Time, Load Weight

Wavesurfer makes use of the Web Audio API now generally available in most modern browsers. Since it uses data from the audio file to generate the waveform, the file first needs to be downloaded before the waveform can be drawn using HTML5 Canvas.

Progress Bar

Since downloading the file can take a few moments (depending on it’s size) it makes sense to include a progress indicator. This isn’t made explicit on the wavesurfer site but can quickly be deduced by inspecting their demo.

wavesurfer.on('loading', function (percents) {
      document.getElementById('progress').value = percents;
    });  
wavesurfer.on('ready', function (percents) {
      document.getElementById('progress').style.display = 'none';
        });  

I settled on some simple CSS reducing the progress bar to an unobtrusive single line at the bottom of the waveform. The progress bar becomes less important however, when enabling the options described below.

Issues

The fact that the file needs to be downloaded before the waveform can be drawn has consequences, especially when it comes to longer pieces of music. It seems unreasonable to impose large download sizes on visitors to a page before they have choosen to listen to the audio.

One solution is to make use of wavesurfer’s MediaElement option – which enables regular HTML5 audio instead of Web Audio – and draw the waveform from a separate waveform file that can be generated using the BBC’s audiowaveform program. 4

Peaks Data

Audiowaveform is easy to run, even for those with little command line experience. There are however two aspects that can be useful to clarify when generating the waveform from peaks data. The example on the wavesurfer site demonstrating how to include the peaks data is incomplete, and it took taking a closer look at some of the codepen examples for me to figure it out. Including waveform data should follow the pattern below. The 'none' option at the end sets the HTML5 audio preload attribute – more on that later.

wavesurfer.load(wavesurfer.song, [-17,18,-62,80,… ], 'none'
            );

Scaling

The second aspect is that the audiowaveform data covers a range of 128 steps (both positive and negative) in describing the waveform peaks, and this data needs to be brought into a range of (+/-) 1 to work within Wavesurfer. One solution is setting Wavesurfer’s Normalize option to true. Another is to scale the audiowaveform data using JavaScript. This can be done by mapping the array as follows:5

wavesurfer.backend.peaks = peaksArray.map(p => p/128);

Since the audiowaveform data files can be quite large, especially when using the audiowaveform defaults on longer sound files, it is advisable to set the pixels-per-second option to 10 or less and the bit depth to 8. I settled on a zoom level as low as 1 pixel-per-second since most of my sound files were at least five minutes in length.6

audiowaveform -i /path/to/myfile.mp3 -o myfile.json --pixels-per-second 1 -b 8

Setting the MediaElement and drawing the waveforms from pre-generated files didn’t solve the initial page-load weight problem though. It turns out that browsers were loading the file even with the preload attribute set to none. More on that issue on GitHub.

Solutions

A solution proposed at the end of the above-mentioned Github issue was loading a silent 1 second file along with the waveform and then loading the correct sound file via JavaScript when play is pressed. That seemed like a bit too much of a hack, so I ended up going with the following Stackoverflow solution.

Summing Up

The resulting script for the sound file on each of my work pages looks something like this:

// Imitate SoundCloud's mirror effect on the waveform. Only works on iOS. (Adapted from the wavesurfer.js demo.) 
var ctx = document.createElement('canvas').getContext('2d');
    var linGrad = ctx.createLinearGradient(0, 56, 0, 200);
    linGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0.88)');
    linGrad.addColorStop(0.5, 'rgba(183, 183, 183, 0.88)');

// Initialize    
var wavesurfer = WaveSurfer.create({
    container: '#waveform',
    scrollParent: false,
    waveColor: linGrad, 
    progressColor: 'rgba(131, 207, 240, 0.5)',
    cursorColor: '#fff',
    cursorWidth: 2,
    height: 128,
    barWidth: 1,
    backend: 'MediaElement'
});

// Load progress
wavesurfer.on('loading', function (percents) {
      document.getElementById('progress').value = percents;
    });
wavesurfer.on('ready', function (percents) {
    document.getElementById('progress').style.display = 'none';
    });

// Define the soundfile
wavesurfer.track = "/path/to/myfile.mp3";

// Set peaks
peaksArray = [-17,18,-62,80,… ];

// Scale peaks
wavesurfer.backend.peaks = peaksArray.map(p => p/128); 

// Draw peaks
wavesurfer.drawBuffer();

// Variable to check if song is loaded
wavesurfer.loaded = false;

// Load song when play is pressed
wavesurfer.on("play", function () {
    if(!wavesurfer.loaded) {
        wavesurfer.load(wavesurfer.song, wavesurfer.backend.peaks
        );
    }
});

// Start playing after song is loaded
wavesurfer.on("ready", function () {
    if(!wavesurfer.loaded) {
        wavesurfer.loaded = true;
        wavesurfer.play();
    }
});;

// Heydon's Play/Pause http://codepen.io/heydon/pen/KWNgEL
const button = document.querySelector('button');
button.addEventListener('click', (e) => {
  let text = e.target.innerText === 'Play' ? 'Pause' : 'Play'
  e.target.innerText = text;
});

// Redraw the waveform when resizing or changing orientation. Enton Biba http://codepen.io/entonbiba/pen/VPqvME
var responsiveWave = wavesurfer.util.debounce(function() {
  wavesurfer.drawBuffer();
}, 150);
window.addEventListener('resize', responsiveWave);      

I’ve added some cosmetic details and included a few lines (via Enton Biba) that redraw the waveform when changing device orientation or resizing the browser window. A similar snippet still needs to be added for regions, should they be included, and an indication of the current playback time could also be worthwhile including. If JavaScript isn’t available there’s a noscript fallback to the HTML5 audio element.7

I can imagine putting it all together in an easy-to-use Kirby plugin/tag, but for the moment that hasn’t been a priority.

Conclusion

Wavesurfer is easy to get up and running when using the Web Audio defaults, but when it comes to loading (potentially long) sound files upfront, one begins to appreciate just how much SoundCloud is taking care of, and generally doing a good job of it.8

I’m pleased however that I found a way of achieving something similar in a self-hosted solution – one that blends in with the rest of the site and also offers further possibilities to shape the results, or extend them with plugins. I can imagine getting a little more creative with it in the future, NPR style.9

A certain degree of know-how is however assumed when it comes to implementing it all, and I hope these notes can be useful to those who, perhaps like myself, find themselves willing to dive into a little code, even if they aren’t “developers” as such.


  1. I find chapter markers an excellent way of distinguishing sections of spoken content, if one’s using a podcast player (such as Overcast) that supports them. I’m not sure how much a quasi-waveform such as the one in the Megaphone player adds to usability, for example

  2. Easliy implemented on my Kirby CMS site using the AudioExt Kirbytag

  3. I have the whole site mirrored in Dropbox so it doubles as my own local archive as well. 

  4. Audiowaveform is run from the command line and can be installed via Homebrew.  

  5. Thanks to Katspaugh for the solution. 

  6. Reducing the number of peaks also helps solve a current issue in which drawing is broken when the canvas size (on a phone for example) is small.  

  7. For some reason the audio element doesn’t work in Safari when JavaScript has been manually disabled, however.  

  8. SoundCloud is apparently also downloading the audio in small chunks – especially handy when it comes to large sound files. 

  9. NPR’s article on the Horrors and Joys of audio in the browser is also worth taking a look at, as is the BBC’s documentation of their prototypes for using waveforms in the browser. 


If you have any comments, questions, or suggestions you can find me on Twitter @RudigerMeyer, send me an email, or send a response via the webmention form below.


Have you published a response to this? (Learn more):

Rudiger Meyer is a composer interested in the play between traditional concert music and new media.