comparison 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
comparison
equal deleted inserted replaced
459:8d561b6df2fa 460:ccce2c09502e
2 import { 2 import {
3 AudioPlayerService, 3 AudioPlayerService,
4 AudioResourceError, 4 AudioResourceError,
5 AudioResource 5 AudioResource
6 } from './services/audio-player/audio-player.service'; 6 } from './services/audio-player/audio-player.service';
7 import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service'; 7 import {
8 ExtractionResult,
9 FeatureExtractionService
10 } from './services/feature-extraction/feature-extraction.service';
8 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component'; 11 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
9 import {DomSanitizer} from '@angular/platform-browser'; 12 import {DomSanitizer} from '@angular/platform-browser';
10 import {MdIconRegistry} from '@angular/material'; 13 import {MdIconRegistry} from '@angular/material';
11 import {Subscription} from 'rxjs/Subscription'; 14 import {Subscription} from 'rxjs/Subscription';
12 import { 15 import {
13 AnalysisItem, 16 AnalysisItem,
14 isPendingAnalysisItem, isPendingRootAudioItem, 17 isPendingAnalysisItem,
15 isRootAudioItem, 18 isPendingRootAudioItem,
19 isLoadedRootAudioItem,
16 Item, 20 Item,
17 PendingAnalysisItem, 21 RootAudioItem,
18 PendingRootAudioItem, 22 LoadedRootAudioItem
19 RootAudioItem 23 } from './analysis-item/AnalysisItem';
20 } from './analysis-item/analysis-item.component';
21 import {OnSeekHandler} from './playhead/PlayHeadHelpers'; 24 import {OnSeekHandler} from './playhead/PlayHeadHelpers';
22 import {UrlResourceLifetimeManager} from './app.module'; 25 import {UrlResourceLifetimeManager} from './app.module';
23 26 import {createExtractionRequest} from './analysis-item/AnalysisItem';
24 class PersistentStack<T> { 27 import {PersistentStack} from './Session';
25 private stack: T[];
26 private history: T[][];
27
28 constructor() {
29 this.stack = [];
30 this.history = [];
31 }
32
33 shift(): T {
34 this.history.push([...this.stack]);
35 const item = this.stack[0];
36 this.stack = this.stack.slice(1);
37 return item;
38 }
39
40 unshift(item: T): number {
41 this.history.push([...this.stack]);
42 this.stack = [item, ...this.stack];
43 return this.stack.length;
44 }
45
46 findIndex(predicate: (value: T,
47 index: number,
48 array: T[]) => boolean): number {
49 return this.stack.findIndex(predicate);
50 }
51
52 filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] {
53 return this.stack.filter(predicate);
54 }
55
56 get(index: number): T {
57 return this.stack[index];
58 }
59
60 set(index: number, value: T) {
61 this.history.push([...this.stack]);
62 this.stack = [
63 ...this.stack.slice(0, index),
64 value,
65 ...this.stack.slice(index + 1)
66 ];
67 }
68
69 map<U>(transform: (value: T, index: number, array: T[]) => U): U[] {
70 return this.stack.map(transform);
71 }
72
73 reduce<U>(reducer: (previousValue: U,
74 currentValue: T,
75 currentIndex: number,
76 array: T[]) => U,
77 initialValue: U): U {
78 return this.stack.reduce(reducer, initialValue);
79 }
80
81 remove(...indices: number[]) {
82 this.history.push([...this.stack]);
83 this.stack = this.stack.reduce((acc, item, i) => {
84 if (!indices.includes(i)) {
85 acc.push(item);
86 }
87 return acc;
88 }, [] as T[]);
89 }
90
91 toIterable(): Iterable<T> {
92 return this.stack;
93 }
94 }
95 28
96 @Component({ 29 @Component({
97 selector: 'ugly-root', 30 selector: 'ugly-root',
98 templateUrl: './app.component.html', 31 templateUrl: './app.component.html',
99 styleUrls: ['./app.component.css'] 32 styleUrls: ['./app.component.css']
103 private onAudioDataSubscription: Subscription; 36 private onAudioDataSubscription: Subscription;
104 private onProgressUpdated: Subscription; 37 private onProgressUpdated: Subscription;
105 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session 38 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
106 private nRecordings: number; // TODO user control for naming a recording 39 private nRecordings: number; // TODO user control for naming a recording
107 private countingId: number; // TODO improve uniquely identifying items 40 private countingId: number; // TODO improve uniquely identifying items
108 private rootAudioItem: RootAudioItem; 41 private rootAudioItem: LoadedRootAudioItem;
109 private onSeek: OnSeekHandler; 42 private onSeek: OnSeekHandler;
110 43
111 constructor(private audioService: AudioPlayerService, 44 constructor(private audioService: AudioPlayerService,
112 private featureService: FeatureExtractionService, 45 private featureService: FeatureExtractionService,
113 private iconRegistry: MdIconRegistry, 46 private iconRegistry: MdIconRegistry,
136 } else { 69 } else {
137 this.rootAudioItem.audioData = (resource as AudioResource).samples; 70 this.rootAudioItem.audioData = (resource as AudioResource).samples;
138 if (this.rootAudioItem.audioData) { 71 if (this.rootAudioItem.audioData) {
139 this.canExtract = true; 72 this.canExtract = true;
140 const currentRootIndex = this.analyses.findIndex(val => { 73 const currentRootIndex = this.analyses.findIndex(val => {
141 return isRootAudioItem(val) && val.uri === this.rootAudioItem.uri; 74 return isLoadedRootAudioItem(val) && val.uri === this.rootAudioItem.uri;
142 }); 75 });
143 if (currentRootIndex !== -1) { 76 if (currentRootIndex !== -1) {
144 this.analyses.set( 77 this.analyses.set(
145 currentRootIndex, 78 currentRootIndex,
146 Object.assign( 79 Object.assign(
194 title: title, 127 title: title,
195 description: new Date().toLocaleString(), 128 description: new Date().toLocaleString(),
196 id: `${++this.countingId}`, 129 id: `${++this.countingId}`,
197 mimeType: file.type, 130 mimeType: file.type,
198 isExportable: createExportableItem 131 isExportable: createExportableItem
199 } as PendingRootAudioItem; 132 } as RootAudioItem;
200 this.rootAudioItem = pending as RootAudioItem; // TODO this is silly 133 this.rootAudioItem = pending as LoadedRootAudioItem; // TODO this is silly
201 134
202 // TODO re-ordering of items for display 135 // TODO re-ordering of items for display
203 // , one alternative is a Angular Pipe / Filter for use in the Template 136 // , one alternative is a Angular Pipe / Filter for use in the Template
204 this.analyses.unshift(pending); 137 this.analyses.unshift(pending);
205 } 138 }
206 139
207 extractFeatures(outputInfo: ExtractorOutputInfo): void { 140 extractFeatures(outputInfo: ExtractorOutputInfo): string {
208 if (!this.canExtract || !outputInfo) { 141 if (!this.canExtract || !outputInfo) {
209 return; 142 return;
210 } 143 }
211 144
212 this.canExtract = false; 145 this.canExtract = false;
213 146
214 const placeholderCard: PendingAnalysisItem = { 147 const placeholderCard: AnalysisItem = {
215 parent: this.rootAudioItem, 148 parent: this.rootAudioItem,
216 hasSharedTimeline: true, 149 hasSharedTimeline: true,
217 extractorKey: outputInfo.combinedKey, 150 extractorKey: outputInfo.extractorKey,
151 outputId: outputInfo.outputId,
218 title: outputInfo.name, 152 title: outputInfo.name,
219 description: outputInfo.outputId, 153 description: outputInfo.outputId,
220 id: `${++this.countingId}`, 154 id: `${++this.countingId}`,
221 progress: 0 155 progress: 0
222 }; 156 };
223 this.analyses.unshift(placeholderCard); 157 this.analyses.unshift(placeholderCard);
224 158 this.sendExtractionRequest(placeholderCard);
225 const audioBuffer = this.rootAudioItem.audioData; 159 return placeholderCard.id;
226 160 }
227 this.featureService.extract(`${this.countingId}`, { 161
228 audioData: [...Array(audioBuffer.numberOfChannels).keys()] 162 removeItem(item: Item): void {
229 .map(i => audioBuffer.getChannelData(i)), 163 const indicesToRemove: number[] = this.analyses.reduce(
230 audioFormat: { 164 (toRemove, current, index) => {
231 sampleRate: audioBuffer.sampleRate, 165 if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
232 channelCount: audioBuffer.numberOfChannels, 166 toRemove.push(index);
233 length: audioBuffer.length 167 } else if (item.id === current.id) {
234 }, 168 toRemove.push(index);
235 key: outputInfo.extractorKey, 169 }
236 outputId: outputInfo.outputId 170 return toRemove;
237 }).then(result => { // TODO subscribe to the extraction service instead 171 }, []);
172 this.analyses.remove(...indicesToRemove);
173 if (isPendingRootAudioItem(item)) {
174 if (this.rootAudioItem.uri === item.uri) {
175 this.audioService.unload();
176 const topItem = this.analyses.get(0);
177 const nullRootAudio: LoadedRootAudioItem = {uri: ''} as any; // TODO eugh
178
179 if (topItem) {
180 if (isPendingAnalysisItem(topItem)) {
181 this.rootAudioItem = topItem.parent as LoadedRootAudioItem;
182 } else if (isPendingRootAudioItem(topItem)) {
183 this.rootAudioItem = topItem as LoadedRootAudioItem;
184 } else {
185 this.rootAudioItem = nullRootAudio;
186 }
187 } else {
188 this.rootAudioItem = nullRootAudio;
189 }
190 if (this.rootAudioItem) {
191 this.audioService.loadAudioFromUri(this.rootAudioItem.uri);
192 }
193 } else {
194 this.resourceManager.revokeUrlToResource(item.uri);
195 }
196 }
197 }
198
199 ngOnDestroy(): void {
200 this.onAudioDataSubscription.unsubscribe();
201 this.onProgressUpdated.unsubscribe();
202 }
203
204 private sendExtractionRequest(analysis: AnalysisItem): Promise<void> {
205 const findAndUpdateItem = (result: ExtractionResult): void => {
206 // TODO subscribe to the extraction service instead
238 const i = this.analyses.findIndex(val => val.id === result.id); 207 const i = this.analyses.findIndex(val => val.id === result.id);
239 this.canExtract = true; 208 this.canExtract = true;
240 if (i !== -1) { 209 if (i !== -1) {
241 this.analyses.set( 210 this.analyses.set(
242 i, 211 i,
246 result.result, 215 result.result,
247 result.unit ? {unit: result.unit} : {} 216 result.unit ? {unit: result.unit} : {}
248 ) 217 )
249 ); 218 );
250 } // TODO else remove the item? 219 } // TODO else remove the item?
251 }).catch(err => { 220 };
252 this.canExtract = true; 221 return this.featureService.extract(
253 this.analyses.shift(); 222 analysis.id,
254 console.error(`Error whilst extracting: ${err}`); 223 createExtractionRequest(analysis))
255 }); 224 .then(findAndUpdateItem)
256 } 225 .catch(err => {
257 226 this.canExtract = true;
258 removeItem(item: Item): void { 227 this.analyses.shift();
259 const indicesToRemove: number[] = this.analyses.reduce( 228 console.error(`Error whilst extracting: ${err}`);
260 (toRemove, current, index) => { 229 });
261 if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
262 toRemove.push(index);
263 } else if (item.id === current.id) {
264 toRemove.push(index);
265 }
266 return toRemove;
267 }, []);
268 this.analyses.remove(...indicesToRemove);
269 if (isPendingRootAudioItem(item)) {
270 if (this.rootAudioItem.uri === item.uri) {
271 this.audioService.unload();
272 const topItem = this.analyses.get(0);
273 const nullRootAudio: RootAudioItem = {uri: ''} as any; // TODO eugh
274
275 if (topItem) {
276 if (isPendingAnalysisItem(topItem)) {
277 this.rootAudioItem = topItem.parent as RootAudioItem;
278 } else if(isPendingRootAudioItem(topItem)) {
279 this.rootAudioItem = topItem as RootAudioItem
280 } else {
281 this.rootAudioItem = nullRootAudio;
282 }
283 } else {
284 this.rootAudioItem = nullRootAudio;
285 }
286 if (this.rootAudioItem) {
287 this.audioService.loadAudioFromUri(this.rootAudioItem.uri);
288 }
289 } else {
290 this.resourceManager.revokeUrlToResource(item.uri);
291 }
292 }
293 }
294
295 ngOnDestroy(): void {
296 this.onAudioDataSubscription.unsubscribe();
297 this.onProgressUpdated.unsubscribe();
298 } 230 }
299 } 231 }