dev@456
|
1 import {Component, Inject, OnDestroy} from '@angular/core';
|
dev@193
|
2 import {
|
dev@193
|
3 AudioPlayerService,
|
dev@456
|
4 AudioResourceError,
|
dev@494
|
5 AudioResource,
|
dev@494
|
6 AudioLoadResponse
|
dev@236
|
7 } from './services/audio-player/audio-player.service';
|
dev@460
|
8 import {
|
dev@460
|
9 ExtractionResult,
|
dev@460
|
10 FeatureExtractionService
|
dev@460
|
11 } from './services/feature-extraction/feature-extraction.service';
|
dev@236
|
12 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
|
dev@89
|
13 import {DomSanitizer} from '@angular/platform-browser';
|
dev@505
|
14 import {MatIconRegistry} from '@angular/material';
|
dev@236
|
15 import {Subscription} from 'rxjs/Subscription';
|
dev@350
|
16 import {
|
dev@353
|
17 AnalysisItem,
|
dev@460
|
18 isPendingAnalysisItem,
|
dev@460
|
19 isPendingRootAudioItem,
|
dev@460
|
20 isLoadedRootAudioItem,
|
dev@456
|
21 Item,
|
dev@460
|
22 RootAudioItem,
|
dev@464
|
23 getRootAudioItem
|
dev@460
|
24 } from './analysis-item/AnalysisItem';
|
dev@347
|
25 import {OnSeekHandler} from './playhead/PlayHeadHelpers';
|
dev@497
|
26 import {UrlResourceLifetimeManager} from './services/File';
|
dev@460
|
27 import {createExtractionRequest} from './analysis-item/AnalysisItem';
|
dev@460
|
28 import {PersistentStack} from './Session';
|
dev@494
|
29 import {NotificationService} from './services/notifications/notifications.service';
|
dev@235
|
30
|
angular-cli@0
|
31 @Component({
|
dev@236
|
32 selector: 'ugly-root',
|
angular-cli@0
|
33 templateUrl: './app.component.html',
|
angular-cli@0
|
34 styleUrls: ['./app.component.css']
|
angular-cli@0
|
35 })
|
dev@193
|
36 export class AppComponent implements OnDestroy {
|
dev@49
|
37 canExtract: boolean;
|
dev@193
|
38 private onAudioDataSubscription: Subscription;
|
dev@226
|
39 private onProgressUpdated: Subscription;
|
dev@350
|
40 private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
|
dev@203
|
41 private nRecordings: number; // TODO user control for naming a recording
|
dev@206
|
42 private countingId: number; // TODO improve uniquely identifying items
|
dev@347
|
43 private onSeek: OnSeekHandler;
|
dev@1
|
44
|
dev@47
|
45 constructor(private audioService: AudioPlayerService,
|
dev@228
|
46 private featureService: FeatureExtractionService,
|
dev@505
|
47 private iconRegistry: MatIconRegistry,
|
dev@456
|
48 private sanitizer: DomSanitizer,
|
dev@456
|
49 @Inject(
|
dev@456
|
50 'UrlResourceLifetimeManager'
|
dev@494
|
51 ) private resourceManager: UrlResourceLifetimeManager,
|
dev@494
|
52 private notifier: NotificationService) {
|
dev@235
|
53 this.analyses = new PersistentStack<AnalysisItem>();
|
dev@49
|
54 this.canExtract = false;
|
dev@203
|
55 this.nRecordings = 0;
|
dev@226
|
56 this.countingId = 0;
|
dev@347
|
57 this.onSeek = (time) => this.audioService.seekTo(time);
|
dev@206
|
58
|
dev@89
|
59 iconRegistry.addSvgIcon(
|
dev@89
|
60 'duck',
|
dev@89
|
61 sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
|
dev@89
|
62 );
|
dev@193
|
63
|
dev@193
|
64 this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
|
dev@193
|
65 resource => {
|
dev@486
|
66 const findCurrentAudio =
|
dev@486
|
67 val => isPendingRootAudioItem(val) && val.uri === getRootAudioItem(
|
dev@486
|
68 this.analyses.get(0)
|
dev@486
|
69 ).uri;
|
dev@494
|
70 const wasError = (res: AudioLoadResponse):
|
dev@494
|
71 res is AudioResourceError => (res as any).message != null;
|
dev@494
|
72 if (wasError(resource)) {
|
dev@494
|
73 this.notifier.displayError(resource.message);
|
dev@486
|
74 this.analyses.findIndexAndUse(
|
dev@486
|
75 findCurrentAudio,
|
dev@486
|
76 index => this.analyses.remove(index)
|
dev@486
|
77 );
|
dev@193
|
78 this.canExtract = false;
|
dev@193
|
79 } else {
|
dev@464
|
80 const audioData = (resource as AudioResource).samples;
|
dev@464
|
81 if (audioData) {
|
dev@193
|
82 this.canExtract = true;
|
dev@486
|
83 this.analyses.findIndexAndUse(
|
dev@486
|
84 findCurrentAudio,
|
dev@486
|
85 currentRootIndex => this.analyses.set(
|
dev@347
|
86 currentRootIndex,
|
dev@347
|
87 Object.assign(
|
dev@347
|
88 {},
|
dev@347
|
89 this.analyses.get(currentRootIndex),
|
dev@464
|
90 {audioData}
|
dev@347
|
91 )
|
dev@486
|
92 ));
|
dev@193
|
93 }
|
dev@193
|
94 }
|
dev@193
|
95 }
|
dev@193
|
96 );
|
dev@228
|
97 this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
|
dev@226
|
98 progress => {
|
dev@486
|
99 this.analyses.findIndexAndUse(
|
dev@486
|
100 val => val.id === progress.id,
|
dev@486
|
101 index => this.analyses.setMutating(
|
dev@486
|
102 index,
|
dev@486
|
103 Object.assign(
|
dev@486
|
104 {},
|
dev@486
|
105 this.analyses.get(index),
|
dev@486
|
106 {progress: progress.value}
|
dev@486
|
107 )
|
dev@235
|
108 )
|
dev@235
|
109 );
|
dev@226
|
110 }
|
dev@226
|
111 );
|
dev@48
|
112 }
|
dev@16
|
113
|
dev@456
|
114 onFileOpened(file: File | Blob, createExportableItem = false) {
|
dev@49
|
115 this.canExtract = false;
|
dev@203
|
116 const url = this.audioService.loadAudio(file);
|
dev@203
|
117 // TODO is it safe to assume it is a recording?
|
dev@203
|
118 const title = (file instanceof File) ?
|
dev@203
|
119 (file as File).name : `Recording ${this.nRecordings++}`;
|
dev@203
|
120
|
dev@203
|
121 if (this.analyses.filter(item => item.title === title).length > 0) {
|
dev@203
|
122 // TODO this reveals how brittle the current name / uri based id is
|
dev@203
|
123 // need something more robust, and also need to notify the user
|
dev@203
|
124 // in a suitable way in the actual event of a duplicate file
|
dev@203
|
125 console.warn('There is already a notebook based on this audio file.');
|
dev@203
|
126 return;
|
dev@203
|
127 }
|
dev@203
|
128
|
dev@350
|
129 const pending = {
|
dev@350
|
130 uri: url,
|
dev@203
|
131 hasSharedTimeline: true,
|
dev@203
|
132 title: title,
|
dev@206
|
133 description: new Date().toLocaleString(),
|
dev@453
|
134 id: `${++this.countingId}`,
|
dev@456
|
135 mimeType: file.type,
|
dev@456
|
136 isExportable: createExportableItem
|
dev@460
|
137 } as RootAudioItem;
|
dev@350
|
138
|
dev@350
|
139 // TODO re-ordering of items for display
|
dev@350
|
140 // , one alternative is a Angular Pipe / Filter for use in the Template
|
dev@466
|
141 this.analyses.unshiftMutating(pending);
|
dev@16
|
142 }
|
dev@47
|
143
|
dev@460
|
144 extractFeatures(outputInfo: ExtractorOutputInfo): string {
|
dev@236
|
145 if (!this.canExtract || !outputInfo) {
|
dev@236
|
146 return;
|
dev@236
|
147 }
|
dev@236
|
148
|
dev@49
|
149 this.canExtract = false;
|
dev@203
|
150
|
dev@464
|
151 const rootAudio = getRootAudioItem(this.analyses.get(0));
|
dev@464
|
152
|
dev@464
|
153 if (isLoadedRootAudioItem(rootAudio)) {
|
dev@464
|
154 const placeholderCard: AnalysisItem = {
|
dev@464
|
155 parent: rootAudio,
|
dev@464
|
156 hasSharedTimeline: true,
|
dev@464
|
157 extractorKey: outputInfo.extractorKey,
|
dev@464
|
158 outputId: outputInfo.outputId,
|
dev@464
|
159 title: outputInfo.name,
|
dev@464
|
160 description: outputInfo.outputId,
|
dev@464
|
161 id: `${++this.countingId}`,
|
dev@464
|
162 progress: 0
|
dev@464
|
163 };
|
dev@466
|
164 this.analyses.unshiftMutating(placeholderCard);
|
dev@464
|
165 this.sendExtractionRequest(placeholderCard);
|
dev@464
|
166 return placeholderCard.id;
|
dev@464
|
167 }
|
dev@464
|
168 throw new Error('Cannot extract. No audio loaded');
|
dev@47
|
169 }
|
dev@193
|
170
|
dev@456
|
171 removeItem(item: Item): void {
|
dev@456
|
172 const indicesToRemove: number[] = this.analyses.reduce(
|
dev@456
|
173 (toRemove, current, index) => {
|
dev@456
|
174 if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
|
dev@456
|
175 toRemove.push(index);
|
dev@456
|
176 } else if (item.id === current.id) {
|
dev@456
|
177 toRemove.push(index);
|
dev@456
|
178 }
|
dev@456
|
179 return toRemove;
|
dev@456
|
180 }, []);
|
dev@459
|
181 this.analyses.remove(...indicesToRemove);
|
dev@456
|
182 }
|
dev@456
|
183
|
dev@193
|
184 ngOnDestroy(): void {
|
dev@193
|
185 this.onAudioDataSubscription.unsubscribe();
|
dev@226
|
186 this.onProgressUpdated.unsubscribe();
|
dev@193
|
187 }
|
dev@460
|
188
|
dev@460
|
189 private sendExtractionRequest(analysis: AnalysisItem): Promise<void> {
|
dev@460
|
190 const findAndUpdateItem = (result: ExtractionResult): void => {
|
dev@460
|
191 // TODO subscribe to the extraction service instead
|
dev@486
|
192 this.analyses.findIndexAndUse(
|
dev@486
|
193 val => val.id === result.id,
|
dev@486
|
194 (index) => this.analyses.set(
|
dev@486
|
195 index,
|
dev@460
|
196 Object.assign(
|
dev@460
|
197 {},
|
dev@486
|
198 this.analyses.get(index),
|
dev@460
|
199 result.result,
|
dev@460
|
200 result.unit ? {unit: result.unit} : {}
|
dev@460
|
201 )
|
dev@486
|
202 )
|
dev@486
|
203 );
|
dev@486
|
204 this.canExtract = true;
|
dev@460
|
205 };
|
dev@460
|
206 return this.featureService.extract(
|
dev@460
|
207 analysis.id,
|
dev@460
|
208 createExtractionRequest(analysis))
|
dev@460
|
209 .then(findAndUpdateItem)
|
dev@460
|
210 .catch(err => {
|
dev@460
|
211 this.canExtract = true;
|
dev@486
|
212 this.analyses.findIndexAndUse(
|
dev@486
|
213 val => val.id === analysis.id,
|
dev@486
|
214 index => this.analyses.remove(index)
|
dev@486
|
215 );
|
dev@494
|
216 this.notifier.displayError(`Error whilst extracting: ${err}`);
|
dev@460
|
217 });
|
dev@460
|
218 }
|
angular-cli@0
|
219 }
|