annotate src/app/app.component.ts @ 349:bf038a51f7e3

Restore some of the feature related logic from waveform.component into a utilities module. Introduce some additional types for representing reshaped features. A work in progress.
author Lucas Thompson <dev@lucas.im>
date Thu, 25 May 2017 17:57:03 +0100
parents 82d476b976e0
children 524f5cd75737
rev   line source
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 }