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:
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.
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:
Viewing it from a component perspective, we can dissect three major sections: the track information, the progress bar and the controls section:
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:
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:
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:
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 theFloat32Array
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
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