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@49
|
102 canExtract: boolean;
|
dev@193
|
103 private onAudioDataSubscription: Subscription;
|
dev@226
|
104 private onProgressUpdated: Subscription;
|
dev@350
|
105 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
|
dev@203
|
106 private nRecordings: number; // TODO user control for naming a recording
|
dev@206
|
107 private countingId: number; // TODO improve uniquely identifying items
|
dev@350
|
108 private rootAudioItem: RootAudioItem;
|
dev@347
|
109 private onSeek: OnSeekHandler;
|
dev@1
|
110
|
dev@47
|
111 constructor(private audioService: AudioPlayerService,
|
dev@228
|
112 private featureService: FeatureExtractionService,
|
dev@89
|
113 private iconRegistry: MdIconRegistry,
|
dev@456
|
114 private sanitizer: DomSanitizer,
|
dev@456
|
115 @Inject(
|
dev@456
|
116 'UrlResourceLifetimeManager'
|
dev@456
|
117 ) private resourceManager: UrlResourceLifetimeManager) {
|
dev@235
|
118 this.analyses = new PersistentStack<AnalysisItem>();
|
dev@49
|
119 this.canExtract = false;
|
dev@203
|
120 this.nRecordings = 0;
|
dev@226
|
121 this.countingId = 0;
|
dev@347
|
122 this.onSeek = (time) => this.audioService.seekTo(time);
|
dev@353
|
123 this.rootAudioItem = {} as any; // TODO eugh
|
dev@206
|
124
|
dev@89
|
125 iconRegistry.addSvgIcon(
|
dev@89
|
126 'duck',
|
dev@89
|
127 sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
|
dev@89
|
128 );
|
dev@193
|
129
|
dev@193
|
130 this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
|
dev@193
|
131 resource => {
|
dev@193
|
132 const wasError = (resource as AudioResourceError).message != null;
|
dev@193
|
133 if (wasError) {
|
dev@203
|
134 this.analyses.shift();
|
dev@193
|
135 this.canExtract = false;
|
dev@193
|
136 } else {
|
dev@459
|
137 this.rootAudioItem.audioData = (resource as AudioResource).samples;
|
dev@459
|
138 if (this.rootAudioItem.audioData) {
|
dev@193
|
139 this.canExtract = true;
|
dev@347
|
140 const currentRootIndex = this.analyses.findIndex(val => {
|
dev@350
|
141 return isRootAudioItem(val) && val.uri === this.rootAudioItem.uri;
|
dev@347
|
142 });
|
dev@347
|
143 if (currentRootIndex !== -1) {
|
dev@347
|
144 this.analyses.set(
|
dev@347
|
145 currentRootIndex,
|
dev@347
|
146 Object.assign(
|
dev@347
|
147 {},
|
dev@347
|
148 this.analyses.get(currentRootIndex),
|
dev@459
|
149 {audioData: this.rootAudioItem.audioData}
|
dev@347
|
150 )
|
dev@347
|
151 );
|
dev@347
|
152 }
|
dev@193
|
153 }
|
dev@193
|
154 }
|
dev@193
|
155 }
|
dev@193
|
156 );
|
dev@228
|
157 this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
|
dev@226
|
158 progress => {
|
dev@226
|
159 const index = this.analyses.findIndex(val => val.id === progress.id);
|
dev@236
|
160 if (index === -1) {
|
dev@236
|
161 return;
|
dev@236
|
162 }
|
dev@235
|
163
|
dev@235
|
164 this.analyses.set(
|
dev@235
|
165 index,
|
dev@235
|
166 Object.assign(
|
dev@235
|
167 {},
|
dev@235
|
168 this.analyses.get(index),
|
dev@235
|
169 {progress: progress.value}
|
dev@235
|
170 )
|
dev@235
|
171 );
|
dev@226
|
172 }
|
dev@226
|
173 );
|
dev@48
|
174 }
|
dev@16
|
175
|
dev@456
|
176 onFileOpened(file: File | Blob, createExportableItem = false) {
|
dev@49
|
177 this.canExtract = false;
|
dev@203
|
178 const url = this.audioService.loadAudio(file);
|
dev@203
|
179 // TODO is it safe to assume it is a recording?
|
dev@203
|
180 const title = (file instanceof File) ?
|
dev@203
|
181 (file as File).name : `Recording ${this.nRecordings++}`;
|
dev@203
|
182
|
dev@203
|
183 if (this.analyses.filter(item => item.title === title).length > 0) {
|
dev@203
|
184 // TODO this reveals how brittle the current name / uri based id is
|
dev@203
|
185 // need something more robust, and also need to notify the user
|
dev@203
|
186 // in a suitable way in the actual event of a duplicate file
|
dev@203
|
187 console.warn('There is already a notebook based on this audio file.');
|
dev@203
|
188 return;
|
dev@203
|
189 }
|
dev@203
|
190
|
dev@350
|
191 const pending = {
|
dev@350
|
192 uri: url,
|
dev@203
|
193 hasSharedTimeline: true,
|
dev@203
|
194 title: title,
|
dev@206
|
195 description: new Date().toLocaleString(),
|
dev@453
|
196 id: `${++this.countingId}`,
|
dev@456
|
197 mimeType: file.type,
|
dev@456
|
198 isExportable: createExportableItem
|
dev@350
|
199 } as PendingRootAudioItem;
|
dev@350
|
200 this.rootAudioItem = pending as RootAudioItem; // TODO this is silly
|
dev@350
|
201
|
dev@350
|
202 // TODO re-ordering of items for display
|
dev@350
|
203 // , one alternative is a Angular Pipe / Filter for use in the Template
|
dev@350
|
204 this.analyses.unshift(pending);
|
dev@16
|
205 }
|
dev@47
|
206
|
dev@48
|
207 extractFeatures(outputInfo: ExtractorOutputInfo): void {
|
dev@236
|
208 if (!this.canExtract || !outputInfo) {
|
dev@236
|
209 return;
|
dev@236
|
210 }
|
dev@236
|
211
|
dev@49
|
212 this.canExtract = false;
|
dev@203
|
213
|
dev@350
|
214 const placeholderCard: PendingAnalysisItem = {
|
dev@350
|
215 parent: this.rootAudioItem,
|
dev@203
|
216 hasSharedTimeline: true,
|
dev@203
|
217 extractorKey: outputInfo.combinedKey,
|
dev@203
|
218 title: outputInfo.name,
|
dev@206
|
219 description: outputInfo.outputId,
|
dev@227
|
220 id: `${++this.countingId}`,
|
dev@227
|
221 progress: 0
|
dev@350
|
222 };
|
dev@350
|
223 this.analyses.unshift(placeholderCard);
|
dev@203
|
224
|
dev@459
|
225 const audioBuffer = this.rootAudioItem.audioData;
|
dev@459
|
226
|
dev@228
|
227 this.featureService.extract(`${this.countingId}`, {
|
dev@459
|
228 audioData: [...Array(audioBuffer.numberOfChannels).keys()]
|
dev@459
|
229 .map(i => audioBuffer.getChannelData(i)),
|
dev@47
|
230 audioFormat: {
|
dev@459
|
231 sampleRate: audioBuffer.sampleRate,
|
dev@459
|
232 channelCount: audioBuffer.numberOfChannels,
|
dev@459
|
233 length: 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@459
|
268 this.analyses.remove(...indicesToRemove);
|
dev@456
|
269 if (isPendingRootAudioItem(item)) {
|
dev@457
|
270 if (this.rootAudioItem.uri === item.uri) {
|
dev@457
|
271 this.audioService.unload();
|
dev@459
|
272 const topItem = this.analyses.get(0);
|
dev@459
|
273 const nullRootAudio: RootAudioItem = {uri: ''} as any; // TODO eugh
|
dev@459
|
274
|
dev@459
|
275 if (topItem) {
|
dev@459
|
276 if (isPendingAnalysisItem(topItem)) {
|
dev@459
|
277 this.rootAudioItem = topItem.parent as RootAudioItem;
|
dev@459
|
278 } else if(isPendingRootAudioItem(topItem)) {
|
dev@459
|
279 this.rootAudioItem = topItem as RootAudioItem
|
dev@459
|
280 } else {
|
dev@459
|
281 this.rootAudioItem = nullRootAudio;
|
dev@459
|
282 }
|
dev@459
|
283 } else {
|
dev@459
|
284 this.rootAudioItem = nullRootAudio;
|
dev@459
|
285 }
|
dev@459
|
286 if (this.rootAudioItem) {
|
dev@459
|
287 this.audioService.loadAudioFromUri(this.rootAudioItem.uri);
|
dev@459
|
288 }
|
dev@457
|
289 } else {
|
dev@457
|
290 this.resourceManager.revokeUrlToResource(item.uri);
|
dev@457
|
291 }
|
dev@456
|
292 }
|
dev@456
|
293 }
|
dev@456
|
294
|
dev@193
|
295 ngOnDestroy(): void {
|
dev@193
|
296 this.onAudioDataSubscription.unsubscribe();
|
dev@226
|
297 this.onProgressUpdated.unsubscribe();
|
dev@193
|
298 }
|
angular-cli@0
|
299 }
|