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