Home Page Component

Find out what logic needs to be implemented for the audio-player home page component.

Let’s turn our attention to the HomePage component and see how the SoundManager service is implemented through this module.

The application home page

Even though the SoundManager service is responsible for implementing the Web Audio API and performing the “heavy lifting” for playback management of tracks, the HomePage is really the hub of the application.

This is where everything is brought together for the user to interact with, nowhere more so than with the application UI.

We’ll start by implementing the logic for this module, particularly with regards to integrating the SoundManager service.

Amendments to the component

In the src/app/home/home.page.ts component class, we make the following changes (additions and amendments are highlighted):

Press + to interact
/**
* HomePage
*
* This class manages the audio playback functionality for the application
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Renderer2, ViewChild } from '@angular/core';
import { SoundManagerService } from '../services/sound-manager.service';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomePage {
@ViewChild('track') public track: ElementRef;
@ViewChild('progress') public progress: ElementRef;
@ViewChild('scrubber') public scrubber: ElementRef;
@ViewChild('preloaderBar') public preloaderBar: ElementRef;
public volume = 1.5;
public panning = 0;
public isPlaying = false;
public currentTrack = 0;
public trackDuration = '';
public currentTime = '';
public trackName = '';
public artistName = '';
public isReversed = false;
public isLoading = false;
public tracksAreLooped = false;
private requestAnimation: any;
private dragging = false;
private startX = 0;
private startLeft = 0;
private playbackPosition = 0;
private durationToggle = false;
private playHeadPosition = 0;
private playbackBarWidth: number;
private playbackTime: number;
public tracks: Array<{artist: string, name: string, track: string}> = [
{
artist : 'Jim Hall',
name : 'Explosions in the sky',
track : '/assets/tracks/explosions-in-the-sky.mp3'
},
{
artist : 'Synth Kid',
name : 'Hope is not lost',
track : '/assets/tracks/hope-is-not-lost.mp3'
},
{
artist : 'Synth Kid',
name : 'Last Breath',
track : '/assets/tracks/last-breath.mp3'
},
{
artist : 'Synth Kid',
name : 'Please Stay',
track : '/assets/tracks/please-stay.mp3'
},
{
artist : 'Synth Kid',
name : 'They mostly come at night',
track : '/assets/tracks/they-mostly-come-at-night.mp3'
}
];
private animationTriggered: number;
private framesPerSecond = 5;
private framesPerSecondInterval: number;
public isMuted = false;
public isVolumeChanged = false;
/**
* Creates an instance of HomePage
*/
constructor(private renderer: Renderer2,
private soundManager: SoundManagerService,
private cdr: ChangeDetectorRef) {}
/**
* Manages the loading of the selected track from the playlist
*/
public loadSound(track: string, index: number): void {
this.isLoading = true;
if (this.soundManager.getSource() !== undefined) {
this.stopPlayback();
}
this.currentTrack = index;
if (!this.isPlaying) {
this.triggerPlayback(track);
} else {
this.triggerPlayback(track);
}
}
/**
* Toggles the track duration timing display
*/
public toggleDuration(): void {
this.durationToggle = !this.durationToggle;
}
/**
* Loads the selected track from the playlist and triggers playback
*/
public triggerPlayback(track: string): void {
this.soundManager.loadSound(track, this.playbackPosition);
setTimeout(() => {
this.manageSoundSettings();
this.animationTriggered = Date.now();
this.framesPerSecondInterval = 1000 / this.framesPerSecond;
this.isLoading = false;
this.isPlaying = true;
this.artistName = ' - ' + this.tracks[this.currentTrack].artist;
this.trackName = this.tracks[this.currentTrack].name;
this.soundManager.getDuration();
this.update();
}, 1000);
}
/**
* Allows the playback audio to be muted/un-muted
*/
public mute(event: any): void {
this.isMuted = event.detail.checked;
if (this.isMuted) {
this.mutePlayback();
} else {
this.soundManager.changeVolume(1.5);
this.volume = 1.5;
}
}
/**
* Mutes all audio for the application
*/
private mutePlayback(): void {
this.soundManager.changeVolume(0);
this.volume = 0;
}
/**
* Allows the volume of the audio output to be altered
*/
public changeVolume(volume: any): void {
this.isVolumeChanged = true;
this.volume = volume.detail.value;
this.soundManager.changeVolume(volume.detail.value);
}
/**
* Allows the speaker balance of the audio output to be altered
*/
public changePanning(panning: any): void {
this.soundManager.changePanning(panning.detail.value);
}
/**
* Toggles the playlist shuffle functionality
*/
public shuffle(event: any): void {
const isChecked = event.detail.checked;
if (isChecked) {
this.shuffleArray(this.tracks);
}
}
/**
* Shuffles the order in which tracks in the playlist are displayed/played
*/
private shuffleArray(arr: Array<{artist: string, name: string, track: string}>): void {
arr.sort(() => Math.random() - 0.5);
// Reset the counter as the playlist order has now changed
this.currentTrack = 0;
}
/**
* Cancels the current track being played and resets the player
*/
public stopPlayback(): void {
this.cancelPlayback();
}
/**
* Allows the previous track (if it exists) in the playlist to be loaded/played
*/
public previousTrack(): void {
// Ensure previous song is cleared from playback :)
this.cancelPlayback();
// Ensure we reset counter for accurate tracking/matching track listings
if (this.currentTrack === this.tracks.length) {
this.currentTrack--;
}
// If this is not the first track in the listing
if (this.currentTrack !== 0) {
this.currentTrack--;
this.loadSound(this.tracks[this.currentTrack].track, this.currentTrack);
}
}
/**
* Allows the next track (if it exists) in the playlist to be loaded/played
*/
public nextTrack(): void {
// Ensure previous song is cleared from playback :)
this.cancelPlayback();
// Increment counter for tracking/accurate matching of next track listing to be played
this.currentTrack++;
// If we have reached the end of the track listings ensure the currentTrack value uses the last array index
if (this.currentTrack === this.tracks.length - 1) {
this.currentTrack = this.tracks.length - 1;
}
// If we are looping the playlist AND we are at the end of the current playlist then we need to reset the play counter
if (this.tracksAreLooped && this.currentTrack === this.tracks.length) {
this.currentTrack = 0;
}
// Load and play
this.loadSound(this.tracks[this.currentTrack].track, this.currentTrack);
}
/**
* Uses the requestAnimationFrame utility to manage the playback display of the following
* items for the track currently being played:
* 1. Progress bar indicating how much of the track has been played
* 2. Movement of playback head in relation to the current position in the track
* 3. Display of current time position of the track (in minutes/seconds)
* 4. Display of length of track (in minutes/seconds)
*/
public update(): void {
this.manageSoundSettings();
// Request animation frame here
this.requestAnimation = window.requestAnimationFrame(() => this.update());
// Set the scrubber width minus 3 pixels (to offset the right hand edge of the scrubber from protruding over
// the end of the progress bar track at the end of playback)
const scrubberWidth = (this.scrubber.nativeElement.offsetWidth - 3);
// Properties for tracking visual rendering of playback duration/progress
const progress = ( this.updatePosition() / this.soundManager.getDuration() );
const width = this.track.nativeElement.offsetWidth - scrubberWidth;
// Determine current position for track playback
this.determinePlaybackTime(width);
// If we have a sound source active then scale the width of the progress bar based on current playback position value
if (this.soundManager.getSource() !== null) {
this.renderer.setStyle(this.progress.nativeElement, 'width', (progress * 100 ) + '%');
}
// If the user is NOT dragging the playhead AND we have a sound source active then move the playhead based on current
// playback position value
if (!this.dragging && this.soundManager.getSource() !== null) {
this.renderer.setStyle(this.scrubber.nativeElement, 'left', (width * progress) + 'px');
this.startLeft = parseInt(this.scrubber.nativeElement.style.left || 0, 10);
}
// Has the playback of the current track completed/or is no longer available?
if (this.soundManager.getSource() === null) {
// Immediately cancel the requestanimationframe - the track has finished
window.cancelAnimationFrame(this.requestAnimation);
// Calculate the time that has elapsed since the last animation frame redraw
const now = Date.now();
const elapsed = now - this.animationTriggered;
// if enough time has elapsed, check to see if we are looping tracks
// If we are then we want to play the next track in the listing
if (elapsed >= this.framesPerSecondInterval) {
if (this.tracksAreLooped) {
this.nextTrack();
}
}
}
this.cdr.detectChanges();
}
/**
* Persists volume/mute settings for the application
*/
private manageSoundSettings(): void {
if (this.isMuted) {
this.mutePlayback();
}
if (this.isVolumeChanged) {
this.soundManager.changeVolume(this.volume);
}
}
/**
* Determines the playback time display
*/
private determineCurrentTime(time: number): void {
const minutes = this.soundManager.getMinutes(time);
const seconds = this.soundManager.getSeconds(time);
this.currentTime = minutes + ':' + seconds;
}
/**
* Determines the current playback time for the track being played
*/
private determinePlaybackTime(width: number): void {
// Properties for tracking visual rendering of playback duration/progress
this.playHeadPosition = parseInt(this.scrubber.nativeElement.style.left || 0, 10);
this.playbackBarWidth = width;
this.playbackTime = this.playHeadPosition / this.playbackBarWidth * this.soundManager.getDuration();
this.determineCurrentTime(this.playbackTime);
}
/**
* Resets all applicable playback values and stops the current track being played
*/
private cancelPlayback(): void {
window.cancelAnimationFrame(this.requestAnimation);
this.currentTime = '0.00';
this.trackDuration = '0.00';
this.renderer.setStyle(this.progress.nativeElement, 'width', 0);
this.renderer.setStyle(this.scrubber.nativeElement, 'left', 0);
this.playbackPosition = 0;
this.trackName = '';
this.artistName = '';
this.dragging = false;
this.isPlaying = false;
this.soundManager.stopSound();
}
/**
* Reverses the playback of a track (MUST be preset BEFORE loading the track to play)
*/
public isPlaybackReversed(event: any): void {
this.soundManager.setReversedState(event.detail.checked);
}
/**
* Sets the value for whether playback of tracks continues on a loop or just one-time only
*/
public loopTracks(event): void {
this.tracksAreLooped = event.detail.checked;
}
/**
* Triggered when the playback head is depressed - prior to dragging
*/
public onDragDown(event: MouseEvent): void {
// Cancel requestionAnimationFrame utility - we need to ensure that the playback is NOT updated whilst we drag
// the playback head across the playback indicator bar
window.cancelAnimationFrame(this.requestAnimation);
// Set up initial property values for tracking/determining dragging
this.dragging = true;
this.startX = event.pageX;
this.startLeft = parseInt(this.scrubber.nativeElement.style.left, 10);
}
/**
* When the playback head is dragged across the playback bar we need to calculate current position
* and update the width of the progress bar and the position of the playback head accordingly
*/
public onDragMove(event: MouseEvent): void {
let width: number;
let position: number;
if ( !this.dragging ) {
return;
}
// Draggable boundary is determined by track - handle width...and 3 pixels for visual tidiness :)
width = this.track.nativeElement.offsetWidth - ((this.scrubber.nativeElement.offsetWidth) - 3);
position = this.startLeft + ( event.pageX - this.startX );
position = Math.max(Math.min(width, position), 0);
const percentage = ((position / width) * 100);
this.renderer.setStyle(this.scrubber.nativeElement, 'left', position + 'px');
this.renderer.setStyle(this.progress.nativeElement, 'width', percentage + '%');
}
/**
* When the playback head is released from being dragged we need to update its position, the width of
* the playback progress bar and determine where the current track will now play from based on the
* current position of the playhead
*/
public onDragRelease(event: MouseEvent): void {
this.playHeadPosition = parseInt(this.scrubber.nativeElement.style.left || 0, 10);
if (this.dragging) {
this.playbackBarWidth = this.track.nativeElement.offsetWidth;
this.playbackTime = this.playHeadPosition / this.playbackBarWidth * this.soundManager.getDuration();
this.renderer.setStyle(this.progress.nativeElement, 'width', ((this.playbackTime / this.soundManager.getDuration() * 100)) + '%');
this.renderer.setStyle(this.scrubber.nativeElement, 'left', this.playHeadPosition + 'px');
this.startLeft = this.playHeadPosition;
this.determinePlaybackOfTrack(this.playbackTime);
this.dragging = false;
}
}
/**
* Manages the playback of the current track after the playhead has been released from being dragged
*/
public determinePlaybackOfTrack(time: number): void {
if (this.isPlaying) {
this.soundManager.stopSound();
this.soundManager.loadSound(this.tracks[this.currentTrack].track, time);
// We need to delay the triggering of the requestAnimationFrame - if we don't the playback bar and playhead
// reset to zero before jumping back to their dragged position (play with the timeout value - see what works best for you)
setTimeout(() => {
this.determineCurrentTime(this.playbackTime);
this.update();
}, 1000);
}
else {
this.playbackPosition = time;
}
}
/**
* Pauses the current track and stores its current playback position for subsequent playing
*/
public pause(): void {
if (this.soundManager.getSource()) {
this.soundManager.pause();
this.playbackPosition = this.soundManager.getTime() - this.soundManager.getStartTime();
this.isPlaying = false;
}
}
/**
* Calculates the current playback position
*/
public updatePosition(): number {
this.playbackPosition = this.isPlaying ? this.soundManager.getTime() - this.soundManager.getStartTime() : this.playbackPosition;
// If we are at the end of the track we might want to pause :)
if ( this.playbackPosition >= this.soundManager.getDuration() ) {
this.playbackPosition = this.soundManager.getDuration();
this.pause();
}
return this.playbackPosition;
}
}

As we can see, there’s a lot going on with our HomePage class!

Let’s take a few moments to break down the core aspects of how this class is managing the user experience and what specific ...