Building an Audio Player with Stimulus

By Exequiel Rozas

Audio isn't the king of multimedia formats, video is. That's why finding a nice audio player isn't always an easy task.

In this article, we will build a custom audio player with Stimulus with customizable controls, responsive waveform visualization and more.

We will be using Stimulus to give the desired functionality to our audio player and ViewComponent to help us with modularity and reusability:

Let's start by seeing what we will build:

What we will build

By the end of this article, we will have a functioning audio player with some typical features an audio player should have plus some extra goodies:

  • Waveform visualization.
  • Appearance customization using component variants.
  • Basic keyboard commands.
  • Responsiveness using the Resize Observer API in Stimulus.
  • Ability to add a playlist.

The final result looks like this:

Building a feature-complete, production-ready audio player is not something to take lightly. By the end of this article, you will have a good understanding of how to do it, but you will probably have some work to do, mainly around edge cases if audio is a central part of your application.

Application setup

Even though an actual audio player is by no means Rails specific, I will be using Rails because of its integration with Stimulus and ViewComponent.

You can accomplish the same we will in the tutorial using Stimulus by itself or even replacing it with vanilla JS, feel free to experiment and let us know how it goes!

Let's start by creating the app:

$ rails new podcast --css=tailwind --javascript=esbuild

Next, we add and install ViewComponent:

$ bundle add view_component && bundle install

Finally, we will also add the class variants gem to handle different styles for our audio player:

$ bundle add class_variants && bundle install

Besides these dependencies, I also included the Font Awesome icon kit for the sake of building the interface a bit faster, but you can use SVGs or any other icon library of your choosing.

Now that we have everything set up, let's start by building the interface.

If you'd like to see how to use the class_variants gem, check our component variants in Rails article, where we dig into the gem to build the initial steps of a design system.

Building the interface

To make the process didactic, we will build the player interface, starting out with a basic progress bar.

Our final result for this section will look like this:

Audio player with Stimulus basic interface

Viewing it from a component perspective, we can dissect three major sections: the track information, the progress bar and the controls section:

Audio player component anatomy

The first step is to create an AudioPlayerComponent:

$ bin/rails generate component AudioPlayer url title author

This will generate a component class and a corresponding empty view. The component should receive three parameters: the URL which is required, the title and author, which will have default values in case they're not present:

class AudioPlayerComponent < ViewComponent::Base
  def initialize(url:, title: "Untitled audio", author: "Unknown author")
    @url = url
    @title = title
    @author = author
  end
end

Now, let's define the basic HTML structure with the container and track info classes to get things started:

<%# app/views/components/audio_player_component.html.erb %>
<div class="<%= container_classes %>">
  <div class="<%= track_info_container_classes %>">
    <h5 class="<%= track_title_classes %>"><%= @title %></h5>
    <p class="<%= track_author_classes %>"><%= @author %></p>
  </div>
</div>

Next, let's add the classes to the base variant using the class_variants gem:

class AudioPlayerComponent < ViewComponent::Base
  def initialize(url:, title: "Untitled audio", author: "Unknown author")
    @url = url
    @title = title
    @author = author
  end

  def container_classes
    ClassVariants.build(
      base: "w-full gap-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-lg py-6 px-8",
    ).render
  end

  def track_info_container_classes
    ClassVariants.build(
      base: "flex flex-col items-center justify-center gap-x-2 mb-3",
    ).render
  end

  def track_title_classes
    ClassVariants.build(
      base: "text-lg font-bold text-slate-800 dark:text-slate-200",
    ).render
  end

  def track_author_classes
    ClassVariants.build(
      base: "text-sm text-slate-500 dark:text-slate-200",
    ).render
  end
end

Now, after rendering the component using <%= render AudioPlayerComponent.new(url: audio_url) in any view, we should get the following:

Basic audio player structure with track title and author

Of course, nothing too exciting for now. Let's add the progress bar with the current_time and duration.

We will use a container to group them, and we will use the --:-- placeholder for the time related attributes:

<div class="<%= container_classes %>">
  <div class="<%= track_info_container_classes %>">
    <h5 class="<%= track_title_classes %>"><%= @title %></h5>
    <p class="<%= track_author_classes %>"><%= @author %></p>
  </div>
  <div class="<%= progress_bar_container_classes %>">
    <div data-audio-player-target="currentTime" class="<%= time_classes %>">--:--</div>
    <div 
      class="<%= progress_bar_classes %>">
      <div class="<%= progress_bar_fill_classes %>" style="width: 24%"></div>
    </div>
    <div class="<%= time_classes %>">--:--</div>
  </div>
</div>

We also add the classes to the component:

def progress_bar_container_classes
  ClassVariants.build(
    base: "flex items-center justify-between gap-x-2",
  ).render
end

def time_classes
  ClassVariants.build(
    base: "text-sm text-slate-800 dark:text-slate-200",
  ).render
end

def progress_bar_classes
  ClassVariants.build(
    base: "flex-grow h-3 bg-slate-200 dark:bg-slate-700 rounded-md overflow-hidden",
  ).render
end

def progress_bar_fill_classes
  ClassVariants.build(
    base: "h-full bg-slate-800 dark:bg-slate-200 animate-progress",
  ).render
end

This should produce the following:

Audio player structure with progress bar

Notice that the progress fill width is hardcoded at this time.

Now, let's finish the interface by adding the icons with their corresponding container:

<div class="<%= container_classes %>">
  <div class="<%= track_info_container_classes %>">
    <h5 class="<%= track_title_classes %>"><%= @title %></h5>
    <p class="<%= track_author_classes %>"><%= @author %></p>
  </div>
  <div class="<%= progress_bar_container_classes %>">
    <div class="<%= time_classes %>">--:--</div>
    <div 
      class="<%= progress_bar_classes %>">
      <div class="<%= progress_bar_fill_classes %>" style="width: 24%"></div>
    </div>
    <div class="<%= time_classes %>">--:--</div>
  </div>
  <div class="<%= controls_container_classes %>">
    <button class="<%= secondary_button_classes %>">
      <i class="far fa-arrow-rotate-left <%= secondary_button_icon_classes %>"></i>
    </button>
    <button class="<%= secondary_button_classes %>">
      <i class="fas fa-step-backward <%= secondary_button_icon_classes %>"></i>
    </button> 
    <button class="<%= play_button_classes %>">
      <i class="fas fa-play <%= play_button_icon_classes %>"></i>
    </button>
    <button class="<%= secondary_button_classes %>">
      <i class="fas fa-step-forward <%= secondary_button_icon_classes %>"></i>
    </button>
    <button class="<%= secondary_button_classes %>">
      <i class="far fa-arrow-rotate-right <%= secondary_button_icon_classes %>"></i>
    </button>
  </div>
</div>

We also add the classes to the component:

def controls_container_classes
  ClassVariants.build(
    base: "flex items-center gap-x-3 justify-center mt-4",
  ).render
end

def play_button_classes
  ClassVariants.build(
    base: "w-10 h-10 rounded-full bg-slate-800 dark:bg-slate-200",
  ).render
end

def play_button_icon_classes
  ClassVariants.build(
    base: "text-white dark:text-slate-800",
  ).render
end

def secondary_button_classes
  ClassVariants.build(
    base: "w-6 h-6 rounded-full",
  ).render
end

def secondary_button_icon_classes
  ClassVariants.build(
    base: "text-slate-800 dark:text-slate-200",
  ).render
end

This produces the same player we saw at the beginning without any functionality:

Completed audio player interface without Stimulus

Now it's time to hook everything up using Stimulus to make the audio player actually work.

But first, let's dive a bit into web audio and how it works:

Understanding Web Audio

As of today, there are mainly two ways to work with audio in the browser: the Web Audio API and HTML 5 Audio.

The former is more powerful, includes many more features, and let's us manipulate the audio more granularly.

Some notable features of the Web Audio API are:

  • Audio graph: it uses a modular, node-based approach to handling audio, where audio flows through a network of connected nodes that have separate functions. The combination of these nodes allows us to manipulate audio in a more granular way.
  • Advanced processing: it supports filters and equalization, reverb, delay, analyzers, oscillators, compression, 3D spatial audio and more.
  • Precise timing: it offers synchronization that's sample-accurate.
  • Access to raw audio data: this means we can manipulate the audio directly by accessing raw audio using the AudioBuffer and the Float32Array features.

However, its power doesn't come without a cost: it's more complex, and can be resource intensive, which might be undesirable in some cases.

On the other hand, HTML 5 Audio is what we use when we define an <audio/> tag with the default browser's audio player

To build an audio player with playback and some visualization capabilities, like we are, HTML 5 Audio is the appropriate choice. However, if you are building something like an online Digital Audio Workstation, virtual instruments or a game that requires the audio playback and response to be more precise, the Web Audio API is definitely the right choice.

Player functionality with Stimulus

Waveform generation

Audio is a more

Waveform placeholder

Audio based waveform generation

Making the waveform responsive

Summary

In this article, we built an audio player with many of the features we expect from a player but we also added the ability to generate waveform visua

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.