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