How to build a SoundCloud-like audio player app with VueJS, Quasar and WaveSurfer
Tags: webdev, cordova, showdev, vue
Published: 2019-07-26 00:00:00

---
date: "2019-07-26"
title: "How to build a SoundCloud-like audio player app with VueJS, Quasar and WaveSurfer"
published: true
category: "Mobile Web Apps"
tags: "webdev, cordova, showdev, vue"
cover_image: "https://www.learningsomethingnew.com/quasar-sc/device_mockup.png"
canonical_url: https://www.learningsomethingnew.com/how-to-build-a-sound-cloud-like-audio-player-app-with-vue-js-quasar-and-wave-surfer
---


## What we're building

![final](https://www.learningsomethingnew.com/quasar-sc/device_mockup.png)

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](https://quasar.dev/quasar-cli/developing-cordova-apps/preparation)

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

```sh
npm install -g @quasar/cli
```

Then create a project:

```sh
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:

```sh
cd quasar-wavesurfer-audio-player
quasar dev
```

![quasar-dev](https://www.learningsomethingnew.com/quasar-sc/quasar-dev.png)

> 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`:

```sh
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](https://www.learningsomethingnew.com/quasar-sc/cordova-mode.png)

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:

```sh
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](https://quasar.dev/quasar-cli/developing-cordova-apps/preparation)

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](https://wavesurfer-js.org/) is an open source audio player that renders the audio wave onto an html5 canvas. Let's add it:

```sh
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:

```html
<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](https://www.learningsomethingnew.com/quasar-sc/adding-waveform.png)

## 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:

```html
<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](https://www.learningsomethingnew.com/quasar-sc/default-file-input.png)

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:
```js
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:

```js
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:
```js
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:

```js
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:
```js
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](https://www.learningsomethingnew.com/quasar-sc/load-file.png)



## Adding controls

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

```html
<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:
```js
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](https://www.learningsomethingnew.com/quasar-sc/controls.png)

## 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`:

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

Change the play button to show only if we're not loading:
```html
<q-btn v-if="!isPlaying && !isLoading"
...
```

Add the `isLoading` member to the `data` section:
```js
isLoading: false
```
And in the `createWaveSurfer()` method, we'll hook into wavesurfer events:
```js
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](https://unsplash.com/photos/c-NBiJrhwdM) as the background. Save the picture as `audio.png` under `assets` folder, and set it as our main div's background using following css:

```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:

```js
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](https://www.learningsomethingnew.com/quasar-sc/device_mockup.png)

## 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.

## Links

- Full source code: [GitHub](https://github.com/syonip/quasar-wavesurfer-audio-player)
- Ask a question / drop a line on twitter at: [@johnnymakestuff](https://twitter.com/johnnymakestuff)
- [Quasar](https://quasar.dev/)
- [WaveSurfer](https://wavesurfer-js.org/)
- Device mockup created with [threed.io](https://threed.io/)

> Originally posted on my [blog](https://www.learningsomethingnew.com/how-to-build-a-sound-cloud-like-audio-player-app-with-vue-js-quasar-and-wave-surfer)