Creating WorkoutAudioComponent for audio support
If we go back and look at the audio cues that are required, there are four distinct audio cues, and hence we are going to create a component with five embedded <audio> tags (two audio tags work together for next-up audio).
From the command line go to the trainer/src/app/workout-runner folder and add a new WorkoutAudioComponent component using Angular CLI.
Open workout-audio.component.html and replace the existing view template with this HTML snippet:
<audio #ticks="MyAudio" loop src="/assets/audio/tick10s.mp3"></audio>
<audio #nextUp="MyAudio" src="/assets/audio/nextup.mp3"></audio>
<audio #nextUpExercise="MyAudio" [src]="'/assets/audio/' + nextupSound"></audio>
<audio #halfway="MyAudio" src="/assets/audio/15seconds.wav"></audio>
<audio #aboutToComplete="MyAudio" src="/assets/audio/321.wav"></audio>
There are five <audio> tags, one for each of the following:
- Ticking audio: The first audio tag produces the ticking sound and is started as soon as the workout starts.
- Next up audio and exercise audio: There next two audio tags work together. The first tag produces the "Next up" sound. And the actual exercise audio is handled by the third tag (in the preceding code snippet).
- Halfway audio: The fourth audio tag plays halfway through the exercise.
- About to complete audio: The final audio tag plays a piece to denote the completion of an exercise.
Did you notice the usage of the # symbol in each of the audio tags? There are some variable assignments prefixed with #. In the Angular world, these variables are known as template reference variables or at times template variables.
As the platform guide defines:
A template reference variable is often a reference to a DOM element or directive within a template.
Look at the last section where MyAudioDirective was defined. The exportAs metadata is set to MyAudio. We repeat that same MyAudio string while assigning the template reference variable for each audio tag:
#ticks="MyAudio"
The role of exportAs is to define the name that can be used in the view to assign this directive to a variable. Remember, a single element/component can have multiple directives applied to it. exportAs allows us to select which directive should be assigned to a template-reference variable based on what is on the right side of equals.
Typically, template variables, once declared, give access to the view element/component they are attached to, to other parts of the view, something we will discuss shortly. But in our case, we will use template variables to refer to the multiple MyAudioDirective from the parent component's code. Let's understand how to use them.
Update the generated workout-audio.compnent.ts with the following outline:
import { Component, OnInit, ViewChild } from '@angular/core';
import { MyAudioDirective } from '../../shared/my-audio.directive';
@Component({
...
})
export class WorkoutAudioComponent implements OnInit {
@ViewChild('ticks') private ticks: MyAudioDirective;
@ViewChild('nextUp') private nextUp: MyAudioDirective;
@ViewChild('nextUpExercise') private nextUpExercise: MyAudioDirective;
@ViewChild('halfway') private halfway: MyAudioDirective;
@ViewChild('aboutToComplete') private aboutToComplete: MyAudioDirective;
private nextupSound: string;
constructor() { }
...
}
The interesting bit in this outline is the @ViewChild decorator against the five properties. The @ViewChild decorator allows us to inject a child component/directive/element reference into its parent. The parameter passed to the decorator is the template variable name, which helps DI match the element/directive to inject. When Angular instantiates the main WorkoutAudioComponent, it injects the corresponding audio directives based on the @ViewChild decorator and the template reference variable name passed. Let's complete the basic class implementation before we look at @ViewChild in detail.
The remaining task is to just play the correct audio component at the right time. Add these functions to WorkoutAudioComponent:
stop() {
this.ticks.stop();
this.nextUp.stop();
this.halfway.stop();
this.aboutToComplete.stop();
this.nextUpExercise.stop();
}
resume() {
this.ticks.start();
if (this.nextUp.currentTime > 0 && !this.nextUp.playbackComplete)
{ this.nextUp.start(); }
else if (this.nextUpExercise.currentTime > 0 && !this.nextUpExercise.playbackComplete)
{ this.nextUpExercise.start(); }
else if (this.halfway.currentTime > 0 && !this.halfway.playbackComplete)
{ this.halfway.start(); }
else if (this.aboutToComplete.currentTime > 0 && !this.aboutToComplete.playbackComplete)
{ this.aboutToComplete.start(); }
}
onExerciseProgress(progress: ExerciseProgressEvent) {
if (progress.runningFor === Math.floor(progress.exercise.duration / 2)
&& progress.exercise.exercise.name != 'rest') {
this.halfway.start();
}
else if (progress.timeRemaining === 3) {
this.aboutToComplete.start();
}
}
onExerciseChanged(state: ExerciseChangedEvent) {
if (state.current.exercise.name === 'rest') {
this.nextupSound = state.next.exercise.nameSound;
setTimeout(() => this.nextUp.start(), 2000);
setTimeout(() => this.nextUpExercise.start(), 3000);
}
}
Having trouble writing these functions? They are available in the checkpoint3.3 Git branch.
There are two new model classes used in the preceding code. Add their declarations to model.ts, as follows (again available in checkpoint3.3):
export class ExerciseProgressEvent {
constructor(
public exercise: ExercisePlan,
public runningFor: number,
public timeRemaining: number,
public workoutTimeRemaining: number) { }
}
export class ExerciseChangedEvent {
constructor(
public current: ExercisePlan,
public next: ExercisePlan) { }
}
These are model classes to track progress events. The WorkoutAudioComponent implementation consumes this data. Remember to import the reference for ExerciseProgressEvent and ExerciseProgressEvent in workout-audio.component.ts.
To reiterate, the audio component consumes the events by defining two event handlers: onExerciseProgress and onExerciseChanged. How the events are generated becomes clear as we move along.
The start and resume functions stop and resume audio whenever a workout starts, pauses, or completes. The extra complexity in the resume function it to tackle cases when the workout was paused during next up, about to complete, or half-way audio playback. We just want to continue from where we left off.
The onExerciseProgress function should be called to report the workout progress. It's used to play the halfway audio and about-to-complete audio based on the state of the workout. The parameter passed to it is an object that contains exercise progress data.
The onExerciseChanged function should be called when the exercise changes. The input parameter contains the current and next exercise in line and helps WorkoutAudioComponent to decide when to play the next up exercise audio.
We touched upon two new concepts in this section: template reference variables and injecting child elements/directives into the parent. It's worth exploring these two concepts in more detail before we continue with the implementation. We'll start with learning more about template reference variables.