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