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