import { Component, ViewChild, ElementRef, OnInit, AfterViewInit, Renderer2, HostListener, Input } from '@angular/core'; import { Visualizer } from './visualizer'; import { AudioSequence } from './audiosequence'; import { OpusWrapper } from './encoding/opus-wrapper'; import { Encoder } from './encoding/encoder'; import { Decoder } from './encoding/decoder'; declare var MediaRecorder: any; @Component({ selector: 'tp-audio-editor', templateUrl: './tp-audio-editor.component.html', styleUrls: ['./tp-audio-editor.component.css'], }) export class TpAudioEditorComponent implements OnInit, AfterViewInit { @Input() set inputBlob(value: Blob) { if (value) { this.decodeBlob(value); } } @ViewChild('visualizer') private canvas: ElementRef; @ViewChild('progressBar') private progressBar: ElementRef; private ctx: CanvasRenderingContext2D; private mediaRecorder: any; private chunks = []; private actx = new AudioContext(); private visualizer: Visualizer; private showInterval = false; private intervalStart = 0; private intervalEnd = 0; private dragStart = 0; private clipboardBuffer: Float32Array[] = []; track: AudioSequence; formats = [ 'wav', 'mp3', 'opus' ]; selectedFormat: string; recordLabel = 'Record'; fileLabel = ''; durationLabel = '00:00 / 00:00'; // Needed to expose the enum to the template PluginState = PluginState; state: PluginState = PluginState.IDLE; constructor(private renderer: Renderer2) { } ngOnInit(): void { this.selectedFormat = this.formats[0]; this.ctx = (this.canvas.nativeElement).getContext('2d'); this.renderer.setProperty( this.canvas.nativeElement, 'width', this.renderer.parentNode(this.canvas.nativeElement).clientWidth ); if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { this.mediaRecorder = new MediaRecorder(stream); this.mediaRecorder.ondataavailable = (e) => { this.chunks.push(e.data); }; this.mediaRecorder.onstop = () => { this.state = PluginState.IDLE; const blob = new Blob(this.chunks, { 'type': 'audio/ogg;codecs=opus' }); this.chunks = []; this.decodeBlob(blob); }; }, err => { console.error(err); }); } } ngAfterViewInit(): void { window.requestAnimationFrame(() => this.repaint()); } public getBlob(): Promise { if (this.track) { return Encoder.encode(this.track.getBuffer(), this.selectedFormat); } else { return Promise.reject('No audio file to get'); } } @HostListener('document:keyup', ['$event']) handleKeyup(event: KeyboardEvent) { if (event.key !== 'Delete' || !this.track) { return; } if ((this.intervalStart === 0 && this.intervalEnd === 0) || !this.showInterval) { return; } this.trim(this.intervalStart, this.intervalEnd); } @HostListener('window:resize', ['$event']) onResize(e: UIEvent): void { // Resize the canvas if the window gets resized this.renderer.setProperty( this.canvas.nativeElement, 'width', this.renderer.parentNode(this.canvas.nativeElement).clientWidth ); } @HostListener('document:copy', ['$event']) onCopy(e: ClipboardEvent): void { if (this.showInterval) { this.clipboardBuffer = this.track.getSection(this.intervalStart, this.intervalEnd); } } @HostListener('document:cut', ['$event']) onCut(e: ClipboardEvent): void { if (this.showInterval) { this.clipboardBuffer = this.track.getSection(this.intervalStart, this.intervalEnd); this.trim(this.intervalStart, this.intervalEnd); } } @HostListener('document:paste', ['$event']) onPaste(e: ClipboardEvent): void { if (!this.showInterval && this.clipboardBuffer.length > 0) { this.track.insert(this.intervalStart, this.clipboardBuffer); this.visualizer.updateBuffer(this.track.getBuffer()); this.stop(); } } mousedown(e: MouseEvent) { if (!this.track) { return; } this.dragStart = this.getXCoordinate(e); } mousemove(e: MouseEvent) { if (!this.track) { return; } const dx = Math.abs(e.clientX - this.dragStart); if (dx >= 5 && e.button === 0 && this.dragStart > 0) { const x = this.getXCoordinate(e); const width = this.canvas.nativeElement.width; this.intervalStart = (Math.min(this.dragStart, x) / width) * this.track.getDuration(); this.intervalEnd = (Math.max(this.dragStart, x) / width) * this.track.getDuration(); this.showInterval = true; } } mouseup(e: MouseEvent) { if (!this.track) { return; } const x = this.getXCoordinate(e); const dx = Math.abs(x - this.dragStart); if (dx < 5) { this.showInterval = false; this.intervalStart = (x / this.canvas.nativeElement.width) * this.track.getDuration(); this.intervalEnd = this.track.getDuration(); } this.track.setCurrentTime(this.intervalStart); this.dragStart = 0; } onFileDropped(files: FileList) { if (this.state !== PluginState.IDLE) { return; } const file = files[0]; if (!this.isValidFile(file)) { return; } this.fileLabel = file.name; this.decodeBlob(file); } fileUploaded(e): void { if (e.target.files.length < 1) { return; } const file = e.target.files[0]; if (!this.isValidFile(file)) { return; } // So that if the same file is uploaded, the onchange event will get fired e.target.value = null; this.fileLabel = file.name; this.decodeBlob(file); } record(): void { if (!this.mediaRecorder) { return; } if (this.mediaRecorder.state === 'inactive') { if (this.track) { this.track.stop(); } this.fileLabel = ''; this.mediaRecorder.start(); this.state = PluginState.RECORDING; this.recordLabel = 'Stop Recording'; } else if (this.mediaRecorder.state === 'recording') { this.mediaRecorder.stop(); this.state = PluginState.IDLE; this.recordLabel = 'Record'; } } play(): void { if (!this.track) { return; } this.track.play(this.intervalStart, this.intervalEnd); } pause(): void { if (!this.track) { return; } this.track.pause(); } stop(): void { if (!this.track) { return; } this.intervalStart = 0; this.intervalEnd = 0; this.showInterval = false; this.track.stop(); } download() { if (!this.track) { return; } this.state = PluginState.DOWNLOADING; this.setIndeterminate(true); const buffer = this.track.getBuffer(); Encoder.encode(buffer, this.selectedFormat).then(blob => { this.state = PluginState.IDLE; this.setProgress(1); this.downloadBlob(blob); }, err => { this.state = PluginState.IDLE; this.setProgress(1); console.error(err.message || err); }); } private getXCoordinate(e: MouseEvent): number { const target = e.currentTarget; return e.pageX - target.offsetLeft; } private setIndeterminate(isIndeterminate: boolean): void { if (isIndeterminate) { this.renderer.removeAttribute(this.progressBar.nativeElement, 'value'); } else { this.setProgress(0); } } private setProgress(progress: number): void { this.renderer.setAttribute(this.progressBar.nativeElement, 'value', '' + progress); } private isValidFile(file: File): boolean { return file && (file.name.endsWith('.mp3') || file.name.endsWith('.wav') || file.name.endsWith('.opus')); } private decodeBlob(blob: Blob | File): void { this.state = PluginState.DECODING; this.setIndeterminate(true); this.clipboardBuffer = []; this.stop(); Decoder.decode(blob, this.actx).then(decodedData => { this.stop(); this.track = new AudioSequence(decodedData, this.actx); if (!this.visualizer) { this.visualizer = new Visualizer(this.canvas, decodedData); } else { this.visualizer.updateBuffer(decodedData); } this.state = PluginState.IDLE; this.setProgress(1); }, err => { this.state = PluginState.IDLE; this.setProgress(1); console.error(err.message || err); }); } private downloadBlob(blob: Blob): void { const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = 'out.' + this.selectedFormat; document.body.appendChild(link); link.click(); document.body.removeChild(link); } private trim(start: number, end: number) { this.track.trim(start, end); this.visualizer.updateBuffer(this.track.getBuffer()); this.showInterval = false; this.stop(); } private formatDuration(current: number, total: number): void { const currentStr = new Date(current * 1000).toISOString().substr(14, 9); const totalStr = new Date(total * 1000).toISOString().substr(14, 9); this.durationLabel = currentStr + ' / ' + totalStr; } private drawPlayhead(): void { if (!this.track) { return; } const x = Math.floor((this.track.getCurrentTime() / this.track.getDuration()) * this.canvas.nativeElement.width); if (x === 0) { return; } this.ctx.save(); this.ctx.strokeStyle = '#008F93'; this.ctx.translate(0, 0); this.ctx.beginPath(); this.ctx.moveTo(x, 0); this.ctx.lineTo(x, this.canvas.nativeElement.height); this.ctx.stroke(); this.ctx.restore(); } private drawInterval(): void { if (!this.showInterval || !this.track) { return; } const x0 = (this.intervalStart / this.track.getDuration()) * this.canvas.nativeElement.width; const x1 = (this.intervalEnd / this.track.getDuration()) * this.canvas.nativeElement.width; this.ctx.save(); this.ctx.fillStyle = 'rgba(80, 133, 229, 0.3)'; this.ctx.fillRect(x0, 0, x1 - x0, this.canvas.nativeElement.height); this.ctx.restore(); } private repaint(): void { this.ctx.save(); this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height); this.ctx.restore(); if (this.visualizer) { this.visualizer.draw(); } this.drawInterval(); this.drawPlayhead(); if (this.track) { this.formatDuration(this.track.getCurrentTime(), this.track.getDuration()); } window.requestAnimationFrame(() => this.repaint()); } } export enum PluginState { IDLE, DOWNLOADING, RECORDING, DECODING }