annotate src/app/app.component.ts @ 427:b0415f8837d9

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