annotate src/app/app.component.ts @ 457:906dd152e333

Unload current audio from the audio player if removed.
author Lucas Thompson <dev@lucas.im>
date Thu, 29 Jun 2017 20:24:31 +0100
parents 7bb0bac6f8dc
children 8d561b6df2fa
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@236 7 import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service';
dev@236 8 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
dev@89 9 import {DomSanitizer} from '@angular/platform-browser';
dev@89 10 import {MdIconRegistry} from '@angular/material';
dev@236 11 import {Subscription} from 'rxjs/Subscription';
dev@350 12 import {
dev@353 13 AnalysisItem,
dev@456 14 isPendingAnalysisItem, isPendingRootAudioItem,
dev@353 15 isRootAudioItem,
dev@456 16 Item,
dev@456 17 PendingAnalysisItem,
dev@456 18 PendingRootAudioItem,
dev@456 19 RootAudioItem
dev@350 20 } from './analysis-item/analysis-item.component';
dev@347 21 import {OnSeekHandler} from './playhead/PlayHeadHelpers';
dev@456 22 import {UrlResourceLifetimeManager} from './app.module';
angular-cli@0 23
dev@235 24 class PersistentStack<T> {
dev@235 25 private stack: T[];
dev@235 26 private history: T[][];
dev@235 27
dev@235 28 constructor() {
dev@235 29 this.stack = [];
dev@235 30 this.history = [];
dev@235 31 }
dev@235 32
dev@235 33 shift(): T {
dev@235 34 this.history.push([...this.stack]);
dev@235 35 const item = this.stack[0];
dev@235 36 this.stack = this.stack.slice(1);
dev@235 37 return item;
dev@235 38 }
dev@235 39
dev@236 40 unshift(item: T): number {
dev@235 41 this.history.push([...this.stack]);
dev@235 42 this.stack = [item, ...this.stack];
dev@235 43 return this.stack.length;
dev@235 44 }
dev@235 45
dev@235 46 findIndex(predicate: (value: T,
dev@235 47 index: number,
dev@235 48 array: T[]) => boolean): number {
dev@235 49 return this.stack.findIndex(predicate);
dev@235 50 }
dev@235 51
dev@235 52 filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] {
dev@235 53 return this.stack.filter(predicate);
dev@235 54 }
dev@235 55
dev@235 56 get(index: number): T {
dev@235 57 return this.stack[index];
dev@235 58 }
dev@235 59
dev@235 60 set(index: number, value: T) {
dev@235 61 this.history.push([...this.stack]);
dev@235 62 this.stack = [
dev@235 63 ...this.stack.slice(0, index),
dev@235 64 value,
dev@235 65 ...this.stack.slice(index + 1)
dev@235 66 ];
dev@235 67 }
dev@235 68
dev@456 69 map<U>(transform: (value: T, index: number, array: T[]) => U): U[] {
dev@456 70 return this.stack.map(transform);
dev@456 71 }
dev@456 72
dev@456 73 reduce<U>(reducer: (previousValue: U,
dev@456 74 currentValue: T,
dev@456 75 currentIndex: number,
dev@456 76 array: T[]) => U,
dev@456 77 initialValue: U): U {
dev@456 78 return this.stack.reduce(reducer, initialValue);
dev@456 79 }
dev@456 80
dev@456 81 remove(...indices: number[]) {
dev@456 82 this.history.push([...this.stack]);
dev@456 83 this.stack = this.stack.reduce((acc, item, i) => {
dev@456 84 if (!indices.includes(i)) {
dev@456 85 acc.push(item);
dev@456 86 }
dev@456 87 return acc;
dev@456 88 }, [] as T[]);
dev@456 89 }
dev@456 90
dev@235 91 toIterable(): Iterable<T> {
dev@235 92 return this.stack;
dev@235 93 }
dev@235 94 }
dev@235 95
angular-cli@0 96 @Component({
dev@236 97 selector: 'ugly-root',
angular-cli@0 98 templateUrl: './app.component.html',
angular-cli@0 99 styleUrls: ['./app.component.css']
angular-cli@0 100 })
dev@193 101 export class AppComponent implements OnDestroy {
dev@31 102 audioBuffer: AudioBuffer; // TODO consider revising
dev@49 103 canExtract: boolean;
dev@193 104 private onAudioDataSubscription: Subscription;
dev@226 105 private onProgressUpdated: Subscription;
dev@350 106 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
dev@203 107 private nRecordings: number; // TODO user control for naming a recording
dev@206 108 private countingId: number; // TODO improve uniquely identifying items
dev@350 109 private rootAudioItem: RootAudioItem;
dev@347 110 private onSeek: OnSeekHandler;
dev@1 111
dev@47 112 constructor(private audioService: AudioPlayerService,
dev@228 113 private featureService: FeatureExtractionService,
dev@89 114 private iconRegistry: MdIconRegistry,
dev@456 115 private sanitizer: DomSanitizer,
dev@456 116 @Inject(
dev@456 117 'UrlResourceLifetimeManager'
dev@456 118 ) private resourceManager: UrlResourceLifetimeManager) {
dev@235 119 this.analyses = new PersistentStack<AnalysisItem>();
dev@49 120 this.canExtract = false;
dev@203 121 this.nRecordings = 0;
dev@226 122 this.countingId = 0;
dev@347 123 this.onSeek = (time) => this.audioService.seekTo(time);
dev@353 124 this.rootAudioItem = {} as any; // TODO eugh
dev@206 125
dev@89 126 iconRegistry.addSvgIcon(
dev@89 127 'duck',
dev@89 128 sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
dev@89 129 );
dev@193 130
dev@193 131 this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
dev@193 132 resource => {
dev@193 133 const wasError = (resource as AudioResourceError).message != null;
dev@193 134 if (wasError) {
dev@203 135 this.analyses.shift();
dev@193 136 this.canExtract = false;
dev@193 137 } else {
dev@193 138 this.audioBuffer = (resource as AudioResource).samples;
dev@350 139 this.rootAudioItem.audioData = this.audioBuffer;
dev@193 140 if (this.audioBuffer) {
dev@193 141 this.canExtract = true;
dev@347 142 const currentRootIndex = this.analyses.findIndex(val => {
dev@350 143 return isRootAudioItem(val) && val.uri === this.rootAudioItem.uri;
dev@347 144 });
dev@347 145 if (currentRootIndex !== -1) {
dev@347 146 this.analyses.set(
dev@347 147 currentRootIndex,
dev@347 148 Object.assign(
dev@347 149 {},
dev@347 150 this.analyses.get(currentRootIndex),
dev@347 151 {audioData: this.audioBuffer}
dev@347 152 )
dev@347 153 );
dev@347 154 }
dev@193 155 }
dev@193 156 }
dev@193 157 }
dev@193 158 );
dev@228 159 this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
dev@226 160 progress => {
dev@226 161 const index = this.analyses.findIndex(val => val.id === progress.id);
dev@236 162 if (index === -1) {
dev@236 163 return;
dev@236 164 }
dev@235 165
dev@235 166 this.analyses.set(
dev@235 167 index,
dev@235 168 Object.assign(
dev@235 169 {},
dev@235 170 this.analyses.get(index),
dev@235 171 {progress: progress.value}
dev@235 172 )
dev@235 173 );
dev@226 174 }
dev@226 175 );
dev@48 176 }
dev@16 177
dev@456 178 onFileOpened(file: File | Blob, createExportableItem = false) {
dev@49 179 this.canExtract = false;
dev@203 180 const url = this.audioService.loadAudio(file);
dev@203 181 // TODO is it safe to assume it is a recording?
dev@203 182 const title = (file instanceof File) ?
dev@203 183 (file as File).name : `Recording ${this.nRecordings++}`;
dev@203 184
dev@203 185 if (this.analyses.filter(item => item.title === title).length > 0) {
dev@203 186 // TODO this reveals how brittle the current name / uri based id is
dev@203 187 // need something more robust, and also need to notify the user
dev@203 188 // in a suitable way in the actual event of a duplicate file
dev@203 189 console.warn('There is already a notebook based on this audio file.');
dev@203 190 return;
dev@203 191 }
dev@203 192
dev@350 193 const pending = {
dev@350 194 uri: url,
dev@203 195 hasSharedTimeline: true,
dev@203 196 title: title,
dev@206 197 description: new Date().toLocaleString(),
dev@453 198 id: `${++this.countingId}`,
dev@456 199 mimeType: file.type,
dev@456 200 isExportable: createExportableItem
dev@350 201 } as PendingRootAudioItem;
dev@350 202 this.rootAudioItem = pending as RootAudioItem; // TODO this is silly
dev@350 203
dev@350 204 // TODO re-ordering of items for display
dev@350 205 // , one alternative is a Angular Pipe / Filter for use in the Template
dev@350 206 this.analyses.unshift(pending);
dev@16 207 }
dev@47 208
dev@48 209 extractFeatures(outputInfo: ExtractorOutputInfo): void {
dev@236 210 if (!this.canExtract || !outputInfo) {
dev@236 211 return;
dev@236 212 }
dev@236 213
dev@49 214 this.canExtract = false;
dev@203 215
dev@350 216 const placeholderCard: PendingAnalysisItem = {
dev@350 217 parent: this.rootAudioItem,
dev@203 218 hasSharedTimeline: true,
dev@203 219 extractorKey: outputInfo.combinedKey,
dev@203 220 title: outputInfo.name,
dev@206 221 description: outputInfo.outputId,
dev@227 222 id: `${++this.countingId}`,
dev@227 223 progress: 0
dev@350 224 };
dev@350 225 this.analyses.unshift(placeholderCard);
dev@203 226
dev@228 227 this.featureService.extract(`${this.countingId}`, {
dev@47 228 audioData: [...Array(this.audioBuffer.numberOfChannels).keys()]
dev@47 229 .map(i => this.audioBuffer.getChannelData(i)),
dev@47 230 audioFormat: {
dev@47 231 sampleRate: this.audioBuffer.sampleRate,
dev@226 232 channelCount: this.audioBuffer.numberOfChannels,
dev@226 233 length: this.audioBuffer.length
dev@47 234 },
dev@47 235 key: outputInfo.extractorKey,
dev@47 236 outputId: outputInfo.outputId
dev@362 237 }).then(result => { // TODO subscribe to the extraction service instead
dev@362 238 const i = this.analyses.findIndex(val => val.id === result.id);
dev@49 239 this.canExtract = true;
dev@362 240 if (i !== -1) {
dev@362 241 this.analyses.set(
dev@362 242 i,
dev@396 243 Object.assign(
dev@396 244 {},
dev@396 245 this.analyses.get(i),
dev@396 246 result.result,
dev@396 247 result.unit ? {unit: result.unit} : {}
dev@396 248 )
dev@362 249 );
dev@362 250 } // TODO else remove the item?
dev@115 251 }).catch(err => {
dev@115 252 this.canExtract = true;
dev@226 253 this.analyses.shift();
dev@226 254 console.error(`Error whilst extracting: ${err}`);
dev@115 255 });
dev@47 256 }
dev@193 257
dev@456 258 removeItem(item: Item): void {
dev@456 259 const indicesToRemove: number[] = this.analyses.reduce(
dev@456 260 (toRemove, current, index) => {
dev@456 261 if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
dev@456 262 toRemove.push(index);
dev@456 263 } else if (item.id === current.id) {
dev@456 264 toRemove.push(index);
dev@456 265 }
dev@456 266 return toRemove;
dev@456 267 }, []);
dev@456 268 if (isPendingRootAudioItem(item)) {
dev@457 269 if (this.rootAudioItem.uri === item.uri) {
dev@457 270 this.audioService.unload();
dev@457 271 } else {
dev@457 272 this.resourceManager.revokeUrlToResource(item.uri);
dev@457 273 }
dev@456 274 }
dev@456 275 this.analyses.remove(...indicesToRemove);
dev@456 276 }
dev@456 277
dev@193 278 ngOnDestroy(): void {
dev@193 279 this.onAudioDataSubscription.unsubscribe();
dev@226 280 this.onProgressUpdated.unsubscribe();
dev@193 281 }
angular-cli@0 282 }