annotate src/app/app.component.ts @ 456:7bb0bac6f8dc

Add export button for recordings and option to remove audio item (also removes all related analyses atm). Revokes associated object url for audio on removal. Will be problematic if the history is used for undo / redo.
author Lucas Thompson <dev@lucas.im>
date Thu, 29 Jun 2017 20:11:14 +0100
parents 8113b6f5a75e
children 906dd152e333
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@456 269 this.resourceManager.revokeUrlToResource(item.uri);
dev@456 270 }
dev@456 271 this.analyses.remove(...indicesToRemove);
dev@456 272 }
dev@456 273
dev@193 274 ngOnDestroy(): void {
dev@193 275 this.onAudioDataSubscription.unsubscribe();
dev@226 276 this.onProgressUpdated.unsubscribe();
dev@193 277 }
angular-cli@0 278 }