annotate src/app/app.component.ts @ 460:ccce2c09502e

Manually cherry-pick various refactoring efforts from feature/basic-session-loading
author Lucas Thompson <dev@lucas.im>
date Fri, 30 Jun 2017 10:41:30 +0100
parents 8d561b6df2fa
children 50f61d1945db
rev   line source
dev@456 1 import {Component, Inject, OnDestroy} from '@angular/core';
dev@193 2 import {
dev@193 3 AudioPlayerService,
dev@456 4 AudioResourceError,
dev@456 5 AudioResource
dev@236 6 } from './services/audio-player/audio-player.service';
dev@460 7 import {
dev@460 8 ExtractionResult,
dev@460 9 FeatureExtractionService
dev@460 10 } from './services/feature-extraction/feature-extraction.service';
dev@236 11 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
dev@89 12 import {DomSanitizer} from '@angular/platform-browser';
dev@89 13 import {MdIconRegistry} from '@angular/material';
dev@236 14 import {Subscription} from 'rxjs/Subscription';
dev@350 15 import {
dev@353 16 AnalysisItem,
dev@460 17 isPendingAnalysisItem,
dev@460 18 isPendingRootAudioItem,
dev@460 19 isLoadedRootAudioItem,
dev@456 20 Item,
dev@460 21 RootAudioItem,
dev@460 22 LoadedRootAudioItem
dev@460 23 } from './analysis-item/AnalysisItem';
dev@347 24 import {OnSeekHandler} from './playhead/PlayHeadHelpers';
dev@456 25 import {UrlResourceLifetimeManager} from './app.module';
dev@460 26 import {createExtractionRequest} from './analysis-item/AnalysisItem';
dev@460 27 import {PersistentStack} from './Session';
dev@235 28
angular-cli@0 29 @Component({
dev@236 30 selector: 'ugly-root',
angular-cli@0 31 templateUrl: './app.component.html',
angular-cli@0 32 styleUrls: ['./app.component.css']
angular-cli@0 33 })
dev@193 34 export class AppComponent implements OnDestroy {
dev@49 35 canExtract: boolean;
dev@193 36 private onAudioDataSubscription: Subscription;
dev@226 37 private onProgressUpdated: Subscription;
dev@350 38 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
dev@203 39 private nRecordings: number; // TODO user control for naming a recording
dev@206 40 private countingId: number; // TODO improve uniquely identifying items
dev@460 41 private rootAudioItem: LoadedRootAudioItem;
dev@347 42 private onSeek: OnSeekHandler;
dev@1 43
dev@47 44 constructor(private audioService: AudioPlayerService,
dev@228 45 private featureService: FeatureExtractionService,
dev@89 46 private iconRegistry: MdIconRegistry,
dev@456 47 private sanitizer: DomSanitizer,
dev@456 48 @Inject(
dev@456 49 'UrlResourceLifetimeManager'
dev@456 50 ) private resourceManager: UrlResourceLifetimeManager) {
dev@235 51 this.analyses = new PersistentStack<AnalysisItem>();
dev@49 52 this.canExtract = false;
dev@203 53 this.nRecordings = 0;
dev@226 54 this.countingId = 0;
dev@347 55 this.onSeek = (time) => this.audioService.seekTo(time);
dev@353 56 this.rootAudioItem = {} as any; // TODO eugh
dev@206 57
dev@89 58 iconRegistry.addSvgIcon(
dev@89 59 'duck',
dev@89 60 sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
dev@89 61 );
dev@193 62
dev@193 63 this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
dev@193 64 resource => {
dev@193 65 const wasError = (resource as AudioResourceError).message != null;
dev@193 66 if (wasError) {
dev@203 67 this.analyses.shift();
dev@193 68 this.canExtract = false;
dev@193 69 } else {
dev@459 70 this.rootAudioItem.audioData = (resource as AudioResource).samples;
dev@459 71 if (this.rootAudioItem.audioData) {
dev@193 72 this.canExtract = true;
dev@347 73 const currentRootIndex = this.analyses.findIndex(val => {
dev@460 74 return isLoadedRootAudioItem(val) && val.uri === this.rootAudioItem.uri;
dev@347 75 });
dev@347 76 if (currentRootIndex !== -1) {
dev@347 77 this.analyses.set(
dev@347 78 currentRootIndex,
dev@347 79 Object.assign(
dev@347 80 {},
dev@347 81 this.analyses.get(currentRootIndex),
dev@459 82 {audioData: this.rootAudioItem.audioData}
dev@347 83 )
dev@347 84 );
dev@347 85 }
dev@193 86 }
dev@193 87 }
dev@193 88 }
dev@193 89 );
dev@228 90 this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
dev@226 91 progress => {
dev@226 92 const index = this.analyses.findIndex(val => val.id === progress.id);
dev@236 93 if (index === -1) {
dev@236 94 return;
dev@236 95 }
dev@235 96
dev@235 97 this.analyses.set(
dev@235 98 index,
dev@235 99 Object.assign(
dev@235 100 {},
dev@235 101 this.analyses.get(index),
dev@235 102 {progress: progress.value}
dev@235 103 )
dev@235 104 );
dev@226 105 }
dev@226 106 );
dev@48 107 }
dev@16 108
dev@456 109 onFileOpened(file: File | Blob, createExportableItem = false) {
dev@49 110 this.canExtract = false;
dev@203 111 const url = this.audioService.loadAudio(file);
dev@203 112 // TODO is it safe to assume it is a recording?
dev@203 113 const title = (file instanceof File) ?
dev@203 114 (file as File).name : `Recording ${this.nRecordings++}`;
dev@203 115
dev@203 116 if (this.analyses.filter(item => item.title === title).length > 0) {
dev@203 117 // TODO this reveals how brittle the current name / uri based id is
dev@203 118 // need something more robust, and also need to notify the user
dev@203 119 // in a suitable way in the actual event of a duplicate file
dev@203 120 console.warn('There is already a notebook based on this audio file.');
dev@203 121 return;
dev@203 122 }
dev@203 123
dev@350 124 const pending = {
dev@350 125 uri: url,
dev@203 126 hasSharedTimeline: true,
dev@203 127 title: title,
dev@206 128 description: new Date().toLocaleString(),
dev@453 129 id: `${++this.countingId}`,
dev@456 130 mimeType: file.type,
dev@456 131 isExportable: createExportableItem
dev@460 132 } as RootAudioItem;
dev@460 133 this.rootAudioItem = pending as LoadedRootAudioItem; // TODO this is silly
dev@350 134
dev@350 135 // TODO re-ordering of items for display
dev@350 136 // , one alternative is a Angular Pipe / Filter for use in the Template
dev@350 137 this.analyses.unshift(pending);
dev@16 138 }
dev@47 139
dev@460 140 extractFeatures(outputInfo: ExtractorOutputInfo): string {
dev@236 141 if (!this.canExtract || !outputInfo) {
dev@236 142 return;
dev@236 143 }
dev@236 144
dev@49 145 this.canExtract = false;
dev@203 146
dev@460 147 const placeholderCard: AnalysisItem = {
dev@350 148 parent: this.rootAudioItem,
dev@203 149 hasSharedTimeline: true,
dev@460 150 extractorKey: outputInfo.extractorKey,
dev@460 151 outputId: outputInfo.outputId,
dev@203 152 title: outputInfo.name,
dev@206 153 description: outputInfo.outputId,
dev@227 154 id: `${++this.countingId}`,
dev@227 155 progress: 0
dev@350 156 };
dev@350 157 this.analyses.unshift(placeholderCard);
dev@460 158 this.sendExtractionRequest(placeholderCard);
dev@460 159 return placeholderCard.id;
dev@47 160 }
dev@193 161
dev@456 162 removeItem(item: Item): void {
dev@456 163 const indicesToRemove: number[] = this.analyses.reduce(
dev@456 164 (toRemove, current, index) => {
dev@456 165 if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
dev@456 166 toRemove.push(index);
dev@456 167 } else if (item.id === current.id) {
dev@456 168 toRemove.push(index);
dev@456 169 }
dev@456 170 return toRemove;
dev@456 171 }, []);
dev@459 172 this.analyses.remove(...indicesToRemove);
dev@456 173 if (isPendingRootAudioItem(item)) {
dev@457 174 if (this.rootAudioItem.uri === item.uri) {
dev@457 175 this.audioService.unload();
dev@459 176 const topItem = this.analyses.get(0);
dev@460 177 const nullRootAudio: LoadedRootAudioItem = {uri: ''} as any; // TODO eugh
dev@459 178
dev@459 179 if (topItem) {
dev@459 180 if (isPendingAnalysisItem(topItem)) {
dev@460 181 this.rootAudioItem = topItem.parent as LoadedRootAudioItem;
dev@460 182 } else if (isPendingRootAudioItem(topItem)) {
dev@460 183 this.rootAudioItem = topItem as LoadedRootAudioItem;
dev@459 184 } else {
dev@459 185 this.rootAudioItem = nullRootAudio;
dev@459 186 }
dev@459 187 } else {
dev@459 188 this.rootAudioItem = nullRootAudio;
dev@459 189 }
dev@459 190 if (this.rootAudioItem) {
dev@459 191 this.audioService.loadAudioFromUri(this.rootAudioItem.uri);
dev@459 192 }
dev@457 193 } else {
dev@457 194 this.resourceManager.revokeUrlToResource(item.uri);
dev@457 195 }
dev@456 196 }
dev@456 197 }
dev@456 198
dev@193 199 ngOnDestroy(): void {
dev@193 200 this.onAudioDataSubscription.unsubscribe();
dev@226 201 this.onProgressUpdated.unsubscribe();
dev@193 202 }
dev@460 203
dev@460 204 private sendExtractionRequest(analysis: AnalysisItem): Promise<void> {
dev@460 205 const findAndUpdateItem = (result: ExtractionResult): void => {
dev@460 206 // TODO subscribe to the extraction service instead
dev@460 207 const i = this.analyses.findIndex(val => val.id === result.id);
dev@460 208 this.canExtract = true;
dev@460 209 if (i !== -1) {
dev@460 210 this.analyses.set(
dev@460 211 i,
dev@460 212 Object.assign(
dev@460 213 {},
dev@460 214 this.analyses.get(i),
dev@460 215 result.result,
dev@460 216 result.unit ? {unit: result.unit} : {}
dev@460 217 )
dev@460 218 );
dev@460 219 } // TODO else remove the item?
dev@460 220 };
dev@460 221 return this.featureService.extract(
dev@460 222 analysis.id,
dev@460 223 createExtractionRequest(analysis))
dev@460 224 .then(findAndUpdateItem)
dev@460 225 .catch(err => {
dev@460 226 this.canExtract = true;
dev@460 227 this.analyses.shift();
dev@460 228 console.error(`Error whilst extracting: ${err}`);
dev@460 229 });
dev@460 230 }
angular-cli@0 231 }