dev@193: import {Component, OnDestroy} from '@angular/core'; dev@193: import { dev@193: AudioPlayerService, dev@193: AudioResourceError, AudioResource dev@236: } from './services/audio-player/audio-player.service'; dev@236: import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service'; dev@236: import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component'; dev@89: import {DomSanitizer} from '@angular/platform-browser'; dev@89: import {MdIconRegistry} from '@angular/material'; dev@236: import {Subscription} from 'rxjs/Subscription'; dev@236: import {AnalysisItem} from './analysis-item/analysis-item.component'; dev@347: import {OnSeekHandler} from './playhead/PlayHeadHelpers'; angular-cli@0: dev@235: class PersistentStack { dev@235: private stack: T[]; dev@235: private history: T[][]; dev@235: dev@235: constructor() { dev@235: this.stack = []; dev@235: this.history = []; dev@235: } dev@235: dev@235: shift(): T { dev@235: this.history.push([...this.stack]); dev@235: const item = this.stack[0]; dev@235: this.stack = this.stack.slice(1); dev@235: return item; dev@235: } dev@235: dev@236: unshift(item: T): number { dev@235: this.history.push([...this.stack]); dev@235: this.stack = [item, ...this.stack]; dev@235: return this.stack.length; dev@235: } dev@235: dev@235: findIndex(predicate: (value: T, dev@235: index: number, dev@235: array: T[]) => boolean): number { dev@235: return this.stack.findIndex(predicate); dev@235: } dev@235: dev@235: filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] { dev@235: return this.stack.filter(predicate); dev@235: } dev@235: dev@235: get(index: number): T { dev@235: return this.stack[index]; dev@235: } dev@235: dev@235: set(index: number, value: T) { dev@235: this.history.push([...this.stack]); dev@235: this.stack = [ dev@235: ...this.stack.slice(0, index), dev@235: value, dev@235: ...this.stack.slice(index + 1) dev@235: ]; dev@235: } dev@235: dev@235: toIterable(): Iterable { dev@235: return this.stack; dev@235: } dev@235: } dev@235: angular-cli@0: @Component({ dev@236: selector: 'ugly-root', angular-cli@0: templateUrl: './app.component.html', angular-cli@0: styleUrls: ['./app.component.css'] angular-cli@0: }) dev@193: export class AppComponent implements OnDestroy { dev@31: audioBuffer: AudioBuffer; // TODO consider revising dev@49: canExtract: boolean; dev@193: private onAudioDataSubscription: Subscription; dev@226: private onProgressUpdated: Subscription; dev@235: private analyses: PersistentStack; // TODO some immutable state container describing entire session dev@203: private nRecordings: number; // TODO user control for naming a recording dev@206: private countingId: number; // TODO improve uniquely identifying items dev@203: private rootAudioUri: string; dev@347: private onSeek: OnSeekHandler; dev@1: dev@47: constructor(private audioService: AudioPlayerService, dev@228: private featureService: FeatureExtractionService, dev@89: private iconRegistry: MdIconRegistry, dev@89: private sanitizer: DomSanitizer) { dev@235: this.analyses = new PersistentStack(); dev@49: this.canExtract = false; dev@203: this.nRecordings = 0; dev@226: this.countingId = 0; dev@347: this.onSeek = (time) => this.audioService.seekTo(time); dev@206: dev@89: iconRegistry.addSvgIcon( dev@89: 'duck', dev@89: sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg') dev@89: ); dev@193: dev@193: this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe( dev@193: resource => { dev@193: const wasError = (resource as AudioResourceError).message != null; dev@193: if (wasError) { dev@203: this.analyses.shift(); dev@193: this.canExtract = false; dev@193: } else { dev@193: this.audioBuffer = (resource as AudioResource).samples; dev@193: if (this.audioBuffer) { dev@193: this.canExtract = true; dev@347: const currentRootIndex = this.analyses.findIndex(val => { dev@347: return val.rootAudioUri === this.rootAudioUri && val.isRoot; dev@347: }); dev@347: if (currentRootIndex !== -1) { dev@347: this.analyses.set( dev@347: currentRootIndex, dev@347: Object.assign( dev@347: {}, dev@347: this.analyses.get(currentRootIndex), dev@347: {audioData: this.audioBuffer} dev@347: ) dev@347: ); dev@347: } dev@193: } dev@193: } dev@193: } dev@193: ); dev@228: this.onProgressUpdated = this.featureService.progressUpdated$.subscribe( dev@226: progress => { dev@226: const index = this.analyses.findIndex(val => val.id === progress.id); dev@236: if (index === -1) { dev@236: return; dev@236: } dev@235: dev@235: this.analyses.set( dev@235: index, dev@235: Object.assign( dev@235: {}, dev@235: this.analyses.get(index), dev@235: {progress: progress.value} dev@235: ) dev@235: ); dev@226: } dev@226: ); dev@48: } dev@16: dev@134: onFileOpened(file: File | Blob) { dev@49: this.canExtract = false; dev@203: const url = this.audioService.loadAudio(file); dev@203: this.rootAudioUri = url; // TODO this isn't going to work to id previously loaded files dev@203: dev@203: // TODO is it safe to assume it is a recording? dev@203: const title = (file instanceof File) ? dev@203: (file as File).name : `Recording ${this.nRecordings++}`; dev@203: dev@203: if (this.analyses.filter(item => item.title === title).length > 0) { dev@203: // TODO this reveals how brittle the current name / uri based id is dev@203: // need something more robust, and also need to notify the user dev@203: // in a suitable way in the actual event of a duplicate file dev@203: console.warn('There is already a notebook based on this audio file.'); dev@203: return; dev@203: } dev@203: dev@203: // TODO re-ordering of items for display dev@203: // , one alternative is a Angular Pipe / Filter for use in the Template dev@203: this.analyses.unshift({ dev@203: rootAudioUri: url, dev@203: hasSharedTimeline: true, dev@203: extractorKey: 'not:real', dev@203: isRoot: true, dev@203: title: title, dev@206: description: new Date().toLocaleString(), dev@226: id: `${++this.countingId}` dev@203: }); dev@16: } dev@47: dev@48: extractFeatures(outputInfo: ExtractorOutputInfo): void { dev@236: if (!this.canExtract || !outputInfo) { dev@236: return; dev@236: } dev@236: dev@49: this.canExtract = false; dev@203: dev@203: this.analyses.unshift({ dev@203: rootAudioUri: this.rootAudioUri, dev@203: hasSharedTimeline: true, dev@203: extractorKey: outputInfo.combinedKey, dev@203: isRoot: false, dev@203: title: outputInfo.name, dev@206: description: outputInfo.outputId, dev@227: id: `${++this.countingId}`, dev@227: progress: 0 dev@203: }); dev@203: dev@228: this.featureService.extract(`${this.countingId}`, { dev@47: audioData: [...Array(this.audioBuffer.numberOfChannels).keys()] dev@47: .map(i => this.audioBuffer.getChannelData(i)), dev@47: audioFormat: { dev@47: sampleRate: this.audioBuffer.sampleRate, dev@226: channelCount: this.audioBuffer.numberOfChannels, dev@226: length: this.audioBuffer.length dev@47: }, dev@47: key: outputInfo.extractorKey, dev@47: outputId: outputInfo.outputId dev@50: }).then(() => { dev@49: this.canExtract = true; dev@115: }).catch(err => { dev@115: this.canExtract = true; dev@226: this.analyses.shift(); dev@226: console.error(`Error whilst extracting: ${err}`); dev@115: }); dev@47: } dev@193: dev@193: ngOnDestroy(): void { dev@193: this.onAudioDataSubscription.unsubscribe(); dev@226: this.onProgressUpdated.unsubscribe(); dev@193: } angular-cli@0: }