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;
+        }
+    }
+
+}