diff --git a/src/core/features/fileuploader/components/audio-histogram/audio-histogram.html b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.html
new file mode 100644
index 000000000..c2e2ad0b6
--- /dev/null
+++ b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.html
@@ -0,0 +1 @@
+
diff --git a/src/core/features/fileuploader/components/audio-histogram/audio-histogram.scss b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.scss
new file mode 100644
index 000000000..bbef26a98
--- /dev/null
+++ b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.scss
@@ -0,0 +1,10 @@
+:host {
+ --background-color: var(--ion-background-color, #fff);
+ --bars-color: var(--ion-text-color, #000);
+
+ canvas {
+ width: 100%;
+ height: 100%;
+ }
+
+}
diff --git a/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts
new file mode 100644
index 000000000..0af7d4e76
--- /dev/null
+++ b/src/core/features/fileuploader/components/audio-histogram/audio-histogram.ts
@@ -0,0 +1,160 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
+
+@Component({
+ selector: 'core-audio-histogram',
+ templateUrl: 'audio-histogram.html',
+ styleUrls: ['audio-histogram.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CoreFileUploaderAudioHistogramComponent implements AfterViewInit, OnDestroy {
+
+ private static readonly BARS_WIDTH = 2;
+ private static readonly BARS_MIN_HEIGHT = 4;
+ private static readonly BARS_GUTTER = 4;
+
+ @Input() analyser!: AnalyserNode;
+ @Input() paused?: boolean;
+ @ViewChild('canvas') canvasRef?: ElementRef;
+
+ private element: HTMLElement;
+ private canvas?: HTMLCanvasElement;
+ private context?: CanvasRenderingContext2D | null;
+ private buffer?: Uint8Array;
+ private destroyed = false;
+
+ constructor({ nativeElement }: ElementRef) {
+ this.element = nativeElement;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ ngAfterViewInit(): void {
+ this.canvas = this.canvasRef?.nativeElement;
+ this.context = this.canvas?.getContext('2d');
+ this.buffer = new Uint8Array(this.analyser.fftSize);
+
+ if (this.context && this.canvas) {
+ const styles = getComputedStyle(this.element);
+
+ this.canvas.width = this.canvas.clientWidth;
+ this.canvas.height = this.canvas.clientHeight;
+ this.context.fillStyle = styles.getPropertyValue('--background-color');
+ this.context.lineCap = 'round';
+ this.context.lineWidth = CoreFileUploaderAudioHistogramComponent.BARS_WIDTH;
+ this.context.strokeStyle = styles.getPropertyValue('--bars-color');
+ }
+
+ this.draw();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ ngOnDestroy(): void {
+ this.destroyed = true;
+ }
+
+ /**
+ * Draw histogram.
+ */
+ private draw(): void {
+ if (this.destroyed || !this.canvas || !this.context || !this.buffer) {
+ return;
+ }
+
+ const width = this.canvas.width;
+ const height = this.canvas.height;
+ const barsWidth = CoreFileUploaderAudioHistogramComponent.BARS_WIDTH;
+ const barsGutter = CoreFileUploaderAudioHistogramComponent.BARS_GUTTER;
+ const chunkLength = Math.floor(this.buffer.length / ((width - barsWidth - 1) / (barsWidth + barsGutter)));
+ const barsCount = Math.floor(this.buffer.length / chunkLength);
+
+ // Reset canvas.
+ this.context.fillRect(0, 0, width, height);
+
+ // Draw bars.
+ const startX = Math.floor((width - (barsWidth + barsGutter)*barsCount - barsWidth - 1)/2);
+
+ this.context.beginPath();
+ this.paused ? this.drawPausedBars(startX) : this.drawActiveBars(startX);
+ this.context.stroke();
+
+ // Schedule next frame.
+ requestAnimationFrame(() => this.draw());
+ }
+
+ /**
+ * Draws bars on the histogram when it is active.
+ *
+ * @param x Starting x position.
+ */
+ private drawActiveBars(x: number): void {
+ if (!this.canvas || !this.context || !this.buffer) {
+ return;
+ }
+
+ let bufferX = 0;
+ const width = this.canvas.width;
+ const halfHeight = this.canvas.height / 2;
+ const halfMinHeight = CoreFileUploaderAudioHistogramComponent.BARS_MIN_HEIGHT / 2;
+ const barsWidth = CoreFileUploaderAudioHistogramComponent.BARS_WIDTH;
+ const barsGutter = CoreFileUploaderAudioHistogramComponent.BARS_GUTTER;
+ const bufferLength = this.buffer.length;
+ const barsBufferWidth = Math.floor(bufferLength / ((width - barsWidth - 1) / (barsWidth + barsGutter)));
+
+ this.analyser.getByteTimeDomainData(this.buffer);
+
+ while (bufferX < bufferLength) {
+ let maxLevel = halfMinHeight;
+
+ do {
+ maxLevel = Math.max(maxLevel, halfHeight * (1 - (this.buffer[bufferX] / 128)));
+ bufferX++;
+ } while (bufferX % barsBufferWidth !== 0 && bufferX < bufferLength);
+
+ this.context.moveTo(x, halfHeight - maxLevel);
+ this.context.lineTo(x, halfHeight + maxLevel);
+
+ x += barsWidth + barsGutter;
+ }
+ }
+
+ /**
+ * Draws bars on the histogram when it is paused.
+ *
+ * @param x Starting x position.
+ */
+ private drawPausedBars(x: number): void {
+ if (!this.canvas || !this.context) {
+ return;
+ }
+
+ const width = this.canvas.width;
+ const halfHeight = this.canvas.height / 2;
+ const halfMinHeight = CoreFileUploaderAudioHistogramComponent.BARS_MIN_HEIGHT / 2;
+ const xStep = CoreFileUploaderAudioHistogramComponent.BARS_WIDTH + CoreFileUploaderAudioHistogramComponent.BARS_GUTTER;
+
+ while (x < width) {
+ this.context.moveTo(x, halfHeight - halfMinHeight);
+ this.context.lineTo(x, halfHeight + halfMinHeight);
+
+ x += xStep;
+ }
+ }
+
+}