Implementing an Audio Level Indicator in Angular
In this blog post, we will discuss how to implement an audio-level indicator component in Angular. The component will visualize the audio level of a specified audio track using a set of rectangles. Let’s dive into the implementation details.
Initializing the Component
Let’s implement the initialization logic for our component in the ngOnInit
method. This method will be called when the component is initialized. Add the following code:
ngOnInit(): void {
combineLatest([
this.audioTrackService.select('audioTrack'),
this.rxState.select('activeAudioInput'),
])
.pipe(
filter(([audioTrack]) => !!audioTrack),
debounceTime(400),
map(([audioTrack]) => {
return audioTrack.mediaStreamTrack;
}),
tap((audioMediaStreamTrack) => {
this.getVolume(audioMediaStreamTrack);
}),
untilDestroyed(this)
)
.subscribe();
}
In the above code, we combine two observables audioTrack
and activeAudioInput
using the combineLatest
operator. We then filter out any falsy values using the filter
operator. After that, we debounce the emitted values using debounceTime
to reduce unnecessary updates. Finally, we map the values to get the mediaStreamTrack
and call the getVolume
method with it. The untilDestroyed
operator ensures that the subscription is automatically unsubscribed when the component is destroyed.
Setting up the View
Now, let’s configure the view for our component. Create an SVG file called audio-level-indicator.component.html
and add the following code:
<svg
xmlns="http://www.w3.org/2000/svg"
width="195"
height="4"
viewBox="0 0 176 3"
>
<g data-name="Group 182628" transform="translate(-8 -441)">
<rect
data-name="Rectangle 149783"
width="16"
height="4"
rx="1"
transform="translate(8 441)"
/>
<rect
data-name="Rectangle 149784"
width="16"
height="4"
rx="1"
transform="translate(28 441)"
/>
<rect
data-name="Rectangle 149785"
width="16"
height="4"
rx="1"
transform="translate(48 441)"
/>
<rect
data-name="Rectangle 149786"
width="16"
height="4"
rx="1"
transform="translate(68 441)"
/>
<rect
data-name="Rectangle 149787"
width="16"
height="4"
rx="1"
transform="translate(88 441)"
/>
<rect
data-name="Rectangle 149788"
width="16"
height="4"
rx="1"
transform="translate(108 441)"
/>
<rect
data-name="Rectangle 149789"
width="16"
height="4"
rx="1"
transform="translate(128 441)"
/>
<rect
data-name="Rectangle 149790"
width="16"
height="4"
rx="1"
transform="translate(148 441)"
/>
<rect
data-name="Rectangle 149791"
width="16"
height="4"
rx="1"
transform="translate(168 441)"
/>
</g>
</svg>
The code above represents an SVG with a group of rectangles. These rectangles will visually represent the audio level.
Updating the View
Next, let’s add the logic to update the colors of the rectangles based on the audio level. Add the following code to your component:
ngAfterViewInit(): void {
this.resetSvgRectColors();
}
// ...
private resetSvgRectColors(rectElements?: NodeListOf<SVGRectElement>): void {
rectElements =
rectElements || this.elementRef.nativeElement.querySelectorAll('rect');
rectElements.forEach((rect: SVGRectElement) => {
this.renderer.setStyle(rect, 'fill', this.volumeColors.outOfRange);
});
}
private updateSvgRectColorsFromSamples(samples: Uint8Array): void {
if (!samples) {
this.resetSvgRectColors();
return;
}
const volume = this.computeVolume(samples);
this.updateSvgRectColors(volume);
}
private updateSvgRectColors(volume: number): void {
const svgElement = this.elementRef.nativeElement.querySelector('svg');
const rectElements = svgElement.querySelectorAll('rect');
this.resetSvgRectColors(rectElements);
const rectCount = rectElements.length;
const volumeLimit = Math.min(volume, rectCount);
for (let i = 0; i < volumeLimit; i++) {
const rect = rectElements[i];
this.renderer.setStyle(rect, 'fill', this.volumeColors.inRange);
}
}
In the code above, we first call resetSvgRectColors
in the ngAfterViewInit
method to initialize the colors of the rectangles. The resetSvgRectColors
the method sets the fill color to outOfRange
color defined in volumeColors
.
The updateSvgRectColorsFromSamples
the method is called whenever there are new audio samples. It checks if samples exist and calls resetSvgRectColors
if they don't. If samples exist, it computes the volume using the computeVolume
method and calls updateSvgRectColors
with the computed volume.
The updateSvgRectColors
method updates the colors of the rectangles based on the volume level. It loops over the rectElements, sets the fill color to inRange
color for the rectangles up to the volume limit, and sets the fill color to outOfRange
for the rest.
Computing the Volume
Lastly, let’s implement the getVolume
, initializeAudioTrackVolumeMeter
, and computeVolume
methods to compute the audio volume. Add the following code to your component:
async getVolume(audioMediaStreamTrack: MediaStreamTrack): Promise<void> {
const { mediaStreamAudioSourceNode, samples } =
await this.initializeAudioTrackVolumeMeter(audioMediaStreamTrack);
const checkVolume = () => {
this.updateSvgRectColorsFromSamples(samples());
if (audioMediaStreamTrack.readyState === 'live') {
requestAnimationFrame(checkVolume);
} else {
mediaStreamAudioSourceNode.disconnect();
this.updateSvgRectColorsFromSamples(null);
}
};
requestAnimationFrame(checkVolume);
}
async initializeAudioTrackVolumeMeter(
audioMediaStreamTrack: MediaStreamTrack
): Promise<{
mediaStreamAudioSourceNode: MediaStreamAudioSourceNode;
samples: Function;
}> {
const audioContext = new AudioContext();
const mediaStreamAudioSourceNode = audioContext.createMediaStreamSource(
new MediaStream([audioMediaStreamTrack])
);
const analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 1024;
analyserNode.smoothingTimeConstant = 0.5;
mediaStreamAudioSourceNode.connect(analyserNode);
const sampleArray = new Uint8Array(analyserNode.frequencyBinCount);
const samples = () => {
analyserNode.getByteFrequencyData(sampleArray);
return sampleArray;
};
return { mediaStreamAudioSourceNode, samples };
}
private computeVolume(samples: Uint8Array): number {
const bufferLength = samples.length;
const values = samples.reduce((acc, curr) => acc + curr, 0);
const logValue = Math.log10(values / bufferLength / 3);
const computedValue = isNaN(logValue) ? 0 : logValue * 7;
const mappedValue = Math.floor((computedValue / this.volumeRange[1]) * 9);
return Math.min(9, Math.max(0, mappedValue));
}
In the code above, the getVolume
method is responsible for setting up the audio track volume meter. It calls initializeAudioTrackVolumeMeter
to initialize the necessary audio nodes and retrieves the sample data. Then, it continuously updates the SVG rectangle colors using updateSvgRectColorsFromSamples
while the audio track is in a live state. If the audio track becomes inactive, it disconnects the audio nodes and resets the SVG rectangle colors.
The initializeAudioTrackVolumeMeter
the method sets up the necessary audio nodes and returns a function to retrieve the sample data from the analyzer node.
The computeVolume
the method computes the volume level based on the audio samples. It sums up the sample values, computes the logarithm, and scales the value to fit within the volume range defined in volumeRange
.
Conclusion
Congratulations! You have successfully implemented an audio level indicator component in Angular. The component visualizes the audio level of a specified audio track using a set of rectangles. It dynamically updates the colors of the rectangles based on the audio volume.
Feel free to customize the component further based on your requirements. You can adjust the number of rectangles, change the colors, or add additional functionality to enhance the audio level indicator.
Happy coding!