dev@193
|
1 import {Component, OnDestroy} from '@angular/core';
|
dev@193
|
2 import {
|
dev@193
|
3 AudioPlayerService,
|
dev@193
|
4 AudioResourceError, AudioResource
|
dev@193
|
5 } from "./services/audio-player/audio-player.service";
|
dev@47
|
6 import {FeatureExtractionService} from "./services/feature-extraction/feature-extraction.service";
|
dev@47
|
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@193
|
10 import {Subscription} from "rxjs";
|
dev@203
|
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@235
|
29 unshift(item: T): number
|
dev@235
|
30 {
|
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({
|
angular-cli@0
|
65 selector: 'app-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@1
|
78
|
dev@47
|
79 constructor(private audioService: AudioPlayerService,
|
dev@228
|
80 private featureService: FeatureExtractionService,
|
dev@89
|
81 private iconRegistry: MdIconRegistry,
|
dev@89
|
82 private sanitizer: DomSanitizer) {
|
dev@235
|
83 this.analyses = new PersistentStack<AnalysisItem>();
|
dev@49
|
84 this.canExtract = false;
|
dev@203
|
85 this.nRecordings = 0;
|
dev@226
|
86 this.countingId = 0;
|
dev@206
|
87
|
dev@89
|
88 iconRegistry.addSvgIcon(
|
dev@89
|
89 'duck',
|
dev@89
|
90 sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
|
dev@89
|
91 );
|
dev@193
|
92
|
dev@193
|
93 this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
|
dev@193
|
94 resource => {
|
dev@193
|
95 const wasError = (resource as AudioResourceError).message != null;
|
dev@193
|
96 if (wasError) {
|
dev@203
|
97 this.analyses.shift();
|
dev@193
|
98 this.canExtract = false;
|
dev@193
|
99 } else {
|
dev@193
|
100 this.audioBuffer = (resource as AudioResource).samples;
|
dev@193
|
101 if (this.audioBuffer) {
|
dev@193
|
102 this.canExtract = true;
|
dev@193
|
103 }
|
dev@193
|
104 }
|
dev@193
|
105 }
|
dev@193
|
106 );
|
dev@228
|
107 this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
|
dev@226
|
108 progress => {
|
dev@226
|
109 const index = this.analyses.findIndex(val => val.id === progress.id);
|
dev@226
|
110 if (index === -1) return;
|
dev@235
|
111
|
dev@235
|
112 this.analyses.set(
|
dev@235
|
113 index,
|
dev@235
|
114 Object.assign(
|
dev@235
|
115 {},
|
dev@235
|
116 this.analyses.get(index),
|
dev@235
|
117 {progress: progress.value}
|
dev@235
|
118 )
|
dev@235
|
119 );
|
dev@226
|
120 }
|
dev@226
|
121 );
|
dev@48
|
122 }
|
dev@16
|
123
|
dev@134
|
124 onFileOpened(file: File | Blob) {
|
dev@49
|
125 this.canExtract = false;
|
dev@203
|
126 const url = this.audioService.loadAudio(file);
|
dev@203
|
127 this.rootAudioUri = url; // TODO this isn't going to work to id previously loaded files
|
dev@203
|
128
|
dev@203
|
129 // TODO is it safe to assume it is a recording?
|
dev@203
|
130 const title = (file instanceof File) ?
|
dev@203
|
131 (file as File).name : `Recording ${this.nRecordings++}`;
|
dev@203
|
132
|
dev@203
|
133 if (this.analyses.filter(item => item.title === title).length > 0) {
|
dev@203
|
134 // TODO this reveals how brittle the current name / uri based id is
|
dev@203
|
135 // need something more robust, and also need to notify the user
|
dev@203
|
136 // in a suitable way in the actual event of a duplicate file
|
dev@203
|
137 console.warn('There is already a notebook based on this audio file.');
|
dev@203
|
138 return;
|
dev@203
|
139 }
|
dev@203
|
140
|
dev@203
|
141 // TODO re-ordering of items for display
|
dev@203
|
142 // , one alternative is a Angular Pipe / Filter for use in the Template
|
dev@203
|
143 this.analyses.unshift({
|
dev@203
|
144 rootAudioUri: url,
|
dev@203
|
145 hasSharedTimeline: true,
|
dev@203
|
146 extractorKey: 'not:real',
|
dev@203
|
147 isRoot: true,
|
dev@203
|
148 title: title,
|
dev@206
|
149 description: new Date().toLocaleString(),
|
dev@226
|
150 id: `${++this.countingId}`
|
dev@203
|
151 });
|
dev@16
|
152 }
|
dev@47
|
153
|
dev@48
|
154 extractFeatures(outputInfo: ExtractorOutputInfo): void {
|
dev@50
|
155 if (!this.canExtract || !outputInfo) return;
|
dev@49
|
156 this.canExtract = false;
|
dev@203
|
157
|
dev@203
|
158 this.analyses.unshift({
|
dev@203
|
159 rootAudioUri: this.rootAudioUri,
|
dev@203
|
160 hasSharedTimeline: true,
|
dev@203
|
161 extractorKey: outputInfo.combinedKey,
|
dev@203
|
162 isRoot: false,
|
dev@203
|
163 title: outputInfo.name,
|
dev@206
|
164 description: outputInfo.outputId,
|
dev@227
|
165 id: `${++this.countingId}`,
|
dev@227
|
166 progress: 0
|
dev@203
|
167 });
|
dev@203
|
168
|
dev@228
|
169 this.featureService.extract(`${this.countingId}`, {
|
dev@47
|
170 audioData: [...Array(this.audioBuffer.numberOfChannels).keys()]
|
dev@47
|
171 .map(i => this.audioBuffer.getChannelData(i)),
|
dev@47
|
172 audioFormat: {
|
dev@47
|
173 sampleRate: this.audioBuffer.sampleRate,
|
dev@226
|
174 channelCount: this.audioBuffer.numberOfChannels,
|
dev@226
|
175 length: this.audioBuffer.length
|
dev@47
|
176 },
|
dev@47
|
177 key: outputInfo.extractorKey,
|
dev@47
|
178 outputId: outputInfo.outputId
|
dev@50
|
179 }).then(() => {
|
dev@49
|
180 this.canExtract = true;
|
dev@115
|
181 }).catch(err => {
|
dev@115
|
182 this.canExtract = true;
|
dev@226
|
183 this.analyses.shift();
|
dev@226
|
184 console.error(`Error whilst extracting: ${err}`);
|
dev@115
|
185 });
|
dev@47
|
186 }
|
dev@193
|
187
|
dev@193
|
188 ngOnDestroy(): void {
|
dev@193
|
189 this.onAudioDataSubscription.unsubscribe();
|
dev@226
|
190 this.onProgressUpdated.unsubscribe();
|
dev@193
|
191 }
|
angular-cli@0
|
192 }
|