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