Mercurial > hg > ugly-duckling
view src/app/app.component.ts @ 236:53ea6406d601
Generate new project with latest @angular/cli, including Angular 4.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Tue, 25 Apr 2017 20:01:09 +0100 |
parents | 76f2cd2c5a68 |
children | 82d476b976e0 |
line wrap: on
line source
import {Component, OnDestroy} from '@angular/core'; import { AudioPlayerService, AudioResourceError, AudioResource } from './services/audio-player/audio-player.service'; import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service'; import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component'; import {DomSanitizer} from '@angular/platform-browser'; import {MdIconRegistry} from '@angular/material'; import {Subscription} from 'rxjs/Subscription'; import {AnalysisItem} from './analysis-item/analysis-item.component'; class PersistentStack<T> { private stack: T[]; private history: T[][]; constructor() { this.stack = []; this.history = []; } shift(): T { this.history.push([...this.stack]); const item = this.stack[0]; this.stack = this.stack.slice(1); return item; } unshift(item: T): number { this.history.push([...this.stack]); this.stack = [item, ...this.stack]; return this.stack.length; } findIndex(predicate: (value: T, index: number, array: T[]) => boolean): number { return this.stack.findIndex(predicate); } filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] { return this.stack.filter(predicate); } get(index: number): T { return this.stack[index]; } set(index: number, value: T) { this.history.push([...this.stack]); this.stack = [ ...this.stack.slice(0, index), value, ...this.stack.slice(index + 1) ]; } toIterable(): Iterable<T> { return this.stack; } } @Component({ selector: 'ugly-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnDestroy { audioBuffer: AudioBuffer; // TODO consider revising canExtract: boolean; private onAudioDataSubscription: Subscription; private onProgressUpdated: Subscription; private analyses: PersistentStack<AnalysisItem>; // TODO some immutable state container describing entire session private nRecordings: number; // TODO user control for naming a recording private countingId: number; // TODO improve uniquely identifying items private rootAudioUri: string; constructor(private audioService: AudioPlayerService, private featureService: FeatureExtractionService, private iconRegistry: MdIconRegistry, private sanitizer: DomSanitizer) { this.analyses = new PersistentStack<AnalysisItem>(); this.canExtract = false; this.nRecordings = 0; this.countingId = 0; iconRegistry.addSvgIcon( 'duck', sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg') ); this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe( resource => { const wasError = (resource as AudioResourceError).message != null; if (wasError) { this.analyses.shift(); this.canExtract = false; } else { this.audioBuffer = (resource as AudioResource).samples; if (this.audioBuffer) { this.canExtract = true; } } } ); this.onProgressUpdated = this.featureService.progressUpdated$.subscribe( progress => { const index = this.analyses.findIndex(val => val.id === progress.id); if (index === -1) { return; } this.analyses.set( index, Object.assign( {}, this.analyses.get(index), {progress: progress.value} ) ); } ); } onFileOpened(file: File | Blob) { this.canExtract = false; const url = this.audioService.loadAudio(file); this.rootAudioUri = url; // TODO this isn't going to work to id previously loaded files // TODO is it safe to assume it is a recording? const title = (file instanceof File) ? (file as File).name : `Recording ${this.nRecordings++}`; if (this.analyses.filter(item => item.title === title).length > 0) { // TODO this reveals how brittle the current name / uri based id is // need something more robust, and also need to notify the user // in a suitable way in the actual event of a duplicate file console.warn('There is already a notebook based on this audio file.'); return; } // TODO re-ordering of items for display // , one alternative is a Angular Pipe / Filter for use in the Template this.analyses.unshift({ rootAudioUri: url, hasSharedTimeline: true, extractorKey: 'not:real', isRoot: true, title: title, description: new Date().toLocaleString(), id: `${++this.countingId}` }); } extractFeatures(outputInfo: ExtractorOutputInfo): void { if (!this.canExtract || !outputInfo) { return; } this.canExtract = false; this.analyses.unshift({ rootAudioUri: this.rootAudioUri, hasSharedTimeline: true, extractorKey: outputInfo.combinedKey, isRoot: false, title: outputInfo.name, description: outputInfo.outputId, id: `${++this.countingId}`, progress: 0 }); this.featureService.extract(`${this.countingId}`, { audioData: [...Array(this.audioBuffer.numberOfChannels).keys()] .map(i => this.audioBuffer.getChannelData(i)), audioFormat: { sampleRate: this.audioBuffer.sampleRate, channelCount: this.audioBuffer.numberOfChannels, length: this.audioBuffer.length }, key: outputInfo.extractorKey, outputId: outputInfo.outputId }).then(() => { this.canExtract = true; }).catch(err => { this.canExtract = true; this.analyses.shift(); console.error(`Error whilst extracting: ${err}`); }); } ngOnDestroy(): void { this.onAudioDataSubscription.unsubscribe(); this.onProgressUpdated.unsubscribe(); } }