Mercurial > hg > ugly-duckling
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 } |