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