Implementing an Audio Level Indicator in Angular

Vinay Somawat
4 min readJun 19, 2023

--

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!

--

--