MOBILE-2314 fileuploader: Create audio-histogram

main
Noel De Martin 2023-02-21 12:15:55 +01:00
parent ba723dd899
commit bb64922a14
3 changed files with 171 additions and 0 deletions

View File

@ -0,0 +1 @@
<canvas #canvas></canvas>

View File

@ -0,0 +1,10 @@
:host {
--background-color: var(--ion-background-color, #fff);
--bars-color: var(--ion-text-color, #000);
canvas {
width: 100%;
height: 100%;
}
}

View File

@ -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<HTMLCanvasElement>;
private element: HTMLElement;
private canvas?: HTMLCanvasElement;
private context?: CanvasRenderingContext2D | null;
private buffer?: Uint8Array;
private destroyed = false;
constructor({ nativeElement }: ElementRef<HTMLElement>) {
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;
}
}
}