How to build a SoundCloud-like audio player app with VueJS, Quasar and WaveSurfer

July 26, 2019

What we’re building

final

We’ll use Quasar and WaveSurfer to build a SoundCloud like cross-platform mobile audio player app. We’ll load a local audio file from the device using html file input, render a waveform and add controls to play the audio.

Quasar?

Quasar is a UI framework built on Vue.js. It seems more mobile-oriented than Vuetify, and also has an excellent CLI that works great for generating Cordova apps. You get debugging on actual device with hot reload out-of-the-box. This is a crucial part of an efficient dev process, and one of the determining factors for me when choosing a framework /stack.

More points that made me want to try it out:

  • Vibrant and active community (11K GitHub stars)
  • Great CLI
  • Beautiful UI components and grid system built in (I like to write as little CSS as possible)
  • Use the Vue ecosystem you know and love

Scaffolding

I assume you already have a working Cordova setup with android studio / xcode configured. Just follow quasar’s tutorial

Let’s start by installing the Quasar CLI. From your workspace terminal run:

npm install -g @quasar/cli

Then create a project:

quasar create quasar-wavesurfer-audio-player

Insert the following answers:

  • Project name: default
  • Project product name: change to “Cool Player”
  • Project Description: A soundcloud-like audio player using quasar and wavesurfer
  • Author: you
  • Features: we don’t need any features
  • Cordova id: leave as is, unless you want to publish to the store(s)

Quasar CLI will install required packages. After it’s done, enter the folder, and run quasar dev to see the basic layout:

cd quasar-wavesurfer-audio-player
quasar dev

quasar-dev

A working version! This is a good time to init a git repo and make the first commit.

Adding the cordova mode

Now we have a Quasar app running in SPA mode. In order to turn it into a mobile app we can then publish to the app store, we need to add Quasar’s cordova mode:

quasar mode add cordova

This Quasar CLI command adds an src-cordova folder, containing our Cordova project. Within this folder we can run any Cordova CLI command. Quasar will build our Vue code and put the assets in src-cordova/www folder.

quasar-dev

Now we’ll check if it’s running on our device. This should be done as early as possible as a sanity check to see that the setup’s working. I’ll use android for this, but it works the same for iOS.

Make sure your device is connected by USB, and run:

quasar dev -m android

Note: Newer chrome versions block cleartext traffic by default, so Cordova’s internal WebView will throw an ERR_CLEARTEXT_NOT_PERMITTED error. To solve this, uncomment https: true, under devServer section in quasar.conf.js

Note2: If you’re building for iOS, it’s recommended to upgrade from the deprecated UIWebView (that Cordova uses by default) to WKWebView using one of the methods described here

Now the CLI asks you which IP to serve on. Choose an IP that’s accessible from your mobile device (they have to be on the same network). Quasar will create a Cordova app that’s inner WebView loads from this IP, thereby giving us the ability to develop with hot reload on our device. This is a crucial feature for an efficient dev process.

We’ll continue developing with quasar dev, while periodically testing on an actual device to see things are working.

If you’re building an app you want to work on android and iOS, I recommend periodically running it on both platforms. They are never the same.

Quasar Project Structure

Now let’s have a look at the project structure the CLI has created for us, for a quick overview of Quasar. Quasar CLI scaffolds a folder structure with the default layout MyLayout.vue inside the layouts folder. The layout is a Vue component that has all the Quasar UI elements including a navigation drawer, a toolbar and a main page. You can spot the Quasar components starting with q- e.g. q-layout, q-toolbar etc. These are all Vue components written by the Quasar team (similar to the v- components of Vuetify).

In order to use a Quasar UI component, you need to explicitly include them in quasar.conf.js, as we’ll see later.
The main page is contained inside the <q-page-container> element in MyLayout.vue, which contains a <router-view /> - the same vue-router you know and love. The routes are saved in the pages folder.

The main page loaded by default is Index.vue under the pages folder, so we’ll add our code there for this example.

Adding wavesurfer.js

WaveSurfer is an open source audio player that renders the audio wave onto an html5 canvas. Let’s add it:

npm i wavesurfer.js

Now we’ll import WaveSurfer into Index.vue, add a wavesurfer data member, and a method createWaveSurfer that will initialize it when the component is mounted. The wavesurfer object needs a container, which is the ID of the html element it will be rendered in. So we’ll also add a container div to the component’s template. We’ll load a demo mp3 file just for testing at this stage. This is how Index.vue looks like now:

<template>
  <div id="waveform"></div>
</template>

<script>
import WaveSurfer from "wavesurfer.js";

export default {
  name: 'PageIndex',
  data: () => ({
    wavesurfer: null,
  }),
  async mounted() {
    if (!this.wavesurfer) this.createWaveSurfer();
  },
  methods: {
    createWaveSurfer() {
      this.wavesurfer = WaveSurfer.create({
        container: "#waveform",
        barWidth: 3
      });
      this.wavesurfer.load(
        "https://ia902606.us.archive.org/35/items/shortpoetry_047_librivox/song_cjrg_teasdale_64kb.mp3"
      );
    }
  }
}
</script>

Running using quasar dev you should see the waveform rendered:

adding-waveform

Loading a local audio file

We’ll use html’s <input type="file"> to load files from the local device. Let’s add a file-loading button to our toolbar. In MyLayout.vue replace <div>Quasar v{{ $q.version }}</div> with a q-btn component:

<q-btn color="white" text-color="primary">
Load File
<input
    type="file"
    class="q-uploader__input overflow-hidden absolute-full"
    v-on:change="fileChosen"
    ref="fileInput"
    accept="audio/mpeg"
/>
</q-btn>

What we have here is a <q-btn> (Quasar button), containing a regular html <input> element, with type="file" (making it a file chooser) that will only accept audio files (accept="audio/mpeg").

Notice that browsers try to enforce file inputs to have one of the internet’s ugliest designs, something like this:

default-file-input

While there is no official way to change the appearance of a file input, we’ll “borrow” the css classes from Quasar’s q-uploader component in order to make it appear more like a button, and less like a relic from the 90’s. Clicking on this in a Cordova app will open the native device’s file choosing interface.

In the methods section add a handler for receiving the file:

fileChosen(file) {
    // Chosen file passed as argument
}

Sending the file to the main page

As we have the file object received in the MyLayout component, but the wavesurfer object in the Index component, we’ll use an event bus to communicate between them and send the file when it’s selected. An event bus can easily be created by using another Vue object. Add a services folder with a new event-bus.js file, containing:

import Vue from 'vue';
export const EventBus = new Vue();

We’ll import it in MyLayout.vue and emit an event whenever the file input value changes:

import { EventBus } from "../services/event-bus.js";

...

methods: {
    fileChosen(file) {
        EventBus.$emit("fileChosen", file);
    }
}

Now we’ll want to catch the event in Index.vue’s mounted handler:

import { EventBus } from "../services/event-bus.js";

...
mounted() {
    ...

    EventBus.$on("fileChosen", file => {
        this.loadFile(file);
    });
}

And finally we’ll load the file using wavesurfer’s loadBlob method:

loadFile(file) {
    if (file.target.files.length == 0) return;

    this.wavesurfer.loadBlob(file.target.files[0]);
}

Try loading a file from your device, and you should see it rendered:

load-file

Adding controls

Let’s add some play/pause/skip buttons. In Index.vue add the following code to the template section:

<template>
  <q-page class>
    <div class="audio-container">
      <div class="row q-ma-md">
        <div class="col-12">
          <div id="waveform"></div>
        </div>
      </div>
    </div>
    <div class="controls row flex flex-center fixed-bottom q-pb-md q-pt-md shadow-10">
      <div class>
        <q-btn
          color="primary"
          flat
          round
          icon="fast_rewind"
          size="xl"
          @click="wavesurfer.skipBackward(1)"
        />
        <q-btn
          v-if="isPlaying"
          color="primary"
          round
          icon="pause"
          size="xl"
          @click="wavesurfer.playPause()"
        />
        <q-btn
          v-if="!isPlaying"
          color="primary"
          round
          icon="play_arrow"
          size="xl"
          @click="wavesurfer.playPause()"
        />
        <q-btn
          color="primary"
          flat
          round
          icon="fast_forward"
          size="xl"
          @click="wavesurfer.skipForward(1)"
        />
      </div>
    </div>
  </q-page>
</template>

And in the script section:

computed: {
    isPlaying() {
      if (!this.wavesurfer) return false;

      return this.wavesurfer.isPlaying();
    }
},

We’ve added some controls using Quasar’s <q-btn> components inside a Quasar "row" div, which arranges them neatly using the positioning helpers flex, fixed-center, and fixed-bottom. q-pb-md/q-pt-md gives us some predefined margins. The @click handlers call wavesurfer’s methods directly.

Notice how we’re designing the UI declaratively using the template section, with Quasar’s modifiers like round, flat and size. This saves us 99% of the css we’d have to write manually.

We’ve also added an isPlaying computed, which is a wrapper around wavesurfer’s method, in order to decide whether to show a play or pause button. It should look like this:

controls

Using wavesurfer hooks to add a loading spinner

Almost done! Now we’ll add a spinner in order to give some feedback to the user when the file is loading. Wavesurfer exposes some hooks so we can handle various events. We’ll use the error, loading and ready events.

In quasar.conf.js components section, add 'QCircularProgress' to the list. Then in Index.vue, above the play button, add a q-circular-progress:

<q-circular-progress v-if="isLoading" size="72px" indeterminate color="primary" />

Change the play button to show only if we’re not loading:

<q-btn v-if="!isPlaying && !isLoading"
...

Add the isLoading member to the data section:

isLoading: false

And in the createWaveSurfer() method, we’ll hook into wavesurfer events:

this.wavesurfer.on("error", err => {
    console.error(err);
    this.isLoading = false;
    this.$q.notify({ message: err });
});

this.wavesurfer.on("loading", () => {
    this.isLoading = true;
});

this.wavesurfer.on("ready", () => {
    this.isLoading = false;
});

We’ve also added a Quasar notification (snackbar) whenever there’s an error. This is made super easy in Quasar using this.$q.notify from anywhere in your code.

A little design

To finalize the design, we’ll add a nice background image and some styling on the waveform. I’ve used James Owen’s picture as the background. Save the picture as audio.png under assets folder, and set it as our main div’s background using following css:

.controls {
  background-color: white;
}
.audio-container {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)),
    url("../assets/audio.jpg") no-repeat center;
  background-size: cover;
}

And some styling for the waveform using WaveSurfer options:

this.wavesurfer = WaveSurfer.create({
    container: "#waveform",
    hideScrollbar: true,
    waveColor: "white",
    progressColor: "hsla(200, 100%, 30%, 0.5)",
    cursorColor: "#fff",
    barWidth: 3
});

And that’s it!

final

Summary

I’ve found Quasar to be a very rapid way of developing a well designed mobile / desktop / web app using Vue, with very little CSS. The CLI is also pretty awesome, and saves a lot of Cordova configuration headache.

WaveSurfer can be customized and has a lot of plugins. We’ve loaded local files for demonstration, but it can also fetch remote urls from your server.

Originally posted on my blog


Written by@Jonathan Perry
Fullstack dev - I like making products fast

GitHubMediumTwitter