Mercurial > hg > ugly-duckling
view src/app/app.component.ts @ 394:f45a916eb5b1
Use the cross hair layer for notes, tracks and curve. This involved bodging in unit to ShapedFeatureData, which isn't particularly easy to do because this isn't an encapsulated type. Need to come back to improving this, as I am monkey-patching a unit property onto Arrays etc.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Thu, 01 Jun 2017 18:55:55 +0100 |
parents | bcb0a172eca3 |
children | 3eab26a629e1 |
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, isRootAudioItem, Item, PendingAnalysisItem, PendingRootAudioItem, RootAudioItem } from './analysis-item/analysis-item.component'; import {OnSeekHandler} from './playhead/PlayHeadHelpers'; 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<Item>; // 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 rootAudioItem: RootAudioItem; private onSeek: OnSeekHandler; 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; this.onSeek = (time) => this.audioService.seekTo(time); this.rootAudioItem = {} as any; // TODO eugh 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; this.rootAudioItem.audioData = this.audioBuffer; if (this.audioBuffer) { this.canExtract = true; const currentRootIndex = this.analyses.findIndex(val => { return isRootAudioItem(val) && val.uri === this.rootAudioItem.uri; }); if (currentRootIndex !== -1) { this.analyses.set( currentRootIndex, Object.assign( {}, this.analyses.get(currentRootIndex), {audioData: this.audioBuffer} ) ); } } } } ); 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); // 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; } const pending = { uri: url, hasSharedTimeline: true, title: title, description: new Date().toLocaleString(), id: `${++this.countingId}` } as PendingRootAudioItem; this.rootAudioItem = pending as RootAudioItem; // TODO this is silly // TODO re-ordering of items for display // , one alternative is a Angular Pipe / Filter for use in the Template this.analyses.unshift(pending); } extractFeatures(outputInfo: ExtractorOutputInfo): void { if (!this.canExtract || !outputInfo) { return; } this.canExtract = false; const placeholderCard: PendingAnalysisItem = { parent: this.rootAudioItem, hasSharedTimeline: true, extractorKey: outputInfo.combinedKey, title: outputInfo.name, description: outputInfo.outputId, id: `${++this.countingId}`, progress: 0 }; this.analyses.unshift(placeholderCard); 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(result => { // TODO subscribe to the extraction service instead const i = this.analyses.findIndex(val => val.id === result.id); this.canExtract = true; if (i !== -1) { this.analyses.set( i, Object.assign({}, this.analyses.get(i), result.result) ); } // TODO else remove the item? }).catch(err => { this.canExtract = true; this.analyses.shift(); console.error(`Error whilst extracting: ${err}`); }); } ngOnDestroy(): void { this.onAudioDataSubscription.unsubscribe(); this.onProgressUpdated.unsubscribe(); } }