dev@456: import {Component, Inject, OnDestroy} from '@angular/core'; dev@193: import { dev@193: AudioPlayerService, dev@456: AudioResourceError, dev@494: AudioResource, dev@494: AudioLoadResponse dev@236: } from './services/audio-player/audio-player.service'; dev@460: import { dev@460: ExtractionResult, dev@460: FeatureExtractionService dev@460: } 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@505: import {MatIconRegistry} from '@angular/material'; dev@236: import {Subscription} from 'rxjs/Subscription'; dev@350: import { dev@353: AnalysisItem, dev@460: isPendingAnalysisItem, dev@460: isPendingRootAudioItem, dev@460: isLoadedRootAudioItem, dev@456: Item, dev@460: RootAudioItem, dev@464: getRootAudioItem dev@460: } from './analysis-item/AnalysisItem'; dev@347: import {OnSeekHandler} from './playhead/PlayHeadHelpers'; dev@497: import {UrlResourceLifetimeManager} from './services/File'; dev@460: import {createExtractionRequest} from './analysis-item/AnalysisItem'; dev@460: import {PersistentStack} from './Session'; dev@494: import {NotificationService} from './services/notifications/notifications.service'; 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@49: canExtract: boolean; dev@193: private onAudioDataSubscription: Subscription; dev@226: private onProgressUpdated: Subscription; dev@350: 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@347: private onSeek: OnSeekHandler; dev@1: dev@47: constructor(private audioService: AudioPlayerService, dev@228: private featureService: FeatureExtractionService, dev@505: private iconRegistry: MatIconRegistry, dev@456: private sanitizer: DomSanitizer, dev@456: @Inject( dev@456: 'UrlResourceLifetimeManager' dev@494: ) private resourceManager: UrlResourceLifetimeManager, dev@494: private notifier: NotificationService) { 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@486: const findCurrentAudio = dev@486: val => isPendingRootAudioItem(val) && val.uri === getRootAudioItem( dev@486: this.analyses.get(0) dev@486: ).uri; dev@494: const wasError = (res: AudioLoadResponse): dev@494: res is AudioResourceError => (res as any).message != null; dev@494: if (wasError(resource)) { dev@494: this.notifier.displayError(resource.message); dev@486: this.analyses.findIndexAndUse( dev@486: findCurrentAudio, dev@486: index => this.analyses.remove(index) dev@486: ); dev@193: this.canExtract = false; dev@193: } else { dev@464: const audioData = (resource as AudioResource).samples; dev@464: if (audioData) { dev@193: this.canExtract = true; dev@486: this.analyses.findIndexAndUse( dev@486: findCurrentAudio, dev@486: currentRootIndex => this.analyses.set( dev@347: currentRootIndex, dev@347: Object.assign( dev@347: {}, dev@347: this.analyses.get(currentRootIndex), dev@464: {audioData} dev@347: ) dev@486: )); dev@193: } dev@193: } dev@193: } dev@193: ); dev@228: this.onProgressUpdated = this.featureService.progressUpdated$.subscribe( dev@226: progress => { dev@486: this.analyses.findIndexAndUse( dev@486: val => val.id === progress.id, dev@486: index => this.analyses.setMutating( dev@486: index, dev@486: Object.assign( dev@486: {}, dev@486: this.analyses.get(index), dev@486: {progress: progress.value} dev@486: ) dev@235: ) dev@235: ); dev@226: } dev@226: ); dev@48: } dev@16: dev@456: onFileOpened(file: File | Blob, createExportableItem = false) { dev@49: this.canExtract = false; dev@203: const url = this.audioService.loadAudio(file); 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@350: const pending = { dev@350: uri: url, dev@203: hasSharedTimeline: true, dev@203: title: title, dev@206: description: new Date().toLocaleString(), dev@453: id: `${++this.countingId}`, dev@456: mimeType: file.type, dev@456: isExportable: createExportableItem dev@460: } as RootAudioItem; dev@350: dev@350: // TODO re-ordering of items for display dev@350: // , one alternative is a Angular Pipe / Filter for use in the Template dev@466: this.analyses.unshiftMutating(pending); dev@16: } dev@47: dev@460: extractFeatures(outputInfo: ExtractorOutputInfo): string { dev@236: if (!this.canExtract || !outputInfo) { dev@236: return; dev@236: } dev@236: dev@49: this.canExtract = false; dev@203: dev@464: const rootAudio = getRootAudioItem(this.analyses.get(0)); dev@464: dev@464: if (isLoadedRootAudioItem(rootAudio)) { dev@464: const placeholderCard: AnalysisItem = { dev@464: parent: rootAudio, dev@464: hasSharedTimeline: true, dev@464: extractorKey: outputInfo.extractorKey, dev@464: outputId: outputInfo.outputId, dev@464: title: outputInfo.name, dev@464: description: outputInfo.outputId, dev@464: id: `${++this.countingId}`, dev@464: progress: 0 dev@464: }; dev@466: this.analyses.unshiftMutating(placeholderCard); dev@464: this.sendExtractionRequest(placeholderCard); dev@464: return placeholderCard.id; dev@464: } dev@464: throw new Error('Cannot extract. No audio loaded'); dev@47: } dev@193: dev@456: removeItem(item: Item): void { dev@456: const indicesToRemove: number[] = this.analyses.reduce( dev@456: (toRemove, current, index) => { dev@456: if (isPendingAnalysisItem(current) && current.parent.id === item.id) { dev@456: toRemove.push(index); dev@456: } else if (item.id === current.id) { dev@456: toRemove.push(index); dev@456: } dev@456: return toRemove; dev@456: }, []); dev@459: this.analyses.remove(...indicesToRemove); dev@456: } dev@456: dev@193: ngOnDestroy(): void { dev@193: this.onAudioDataSubscription.unsubscribe(); dev@226: this.onProgressUpdated.unsubscribe(); dev@193: } dev@460: dev@460: private sendExtractionRequest(analysis: AnalysisItem): Promise { dev@460: const findAndUpdateItem = (result: ExtractionResult): void => { dev@460: // TODO subscribe to the extraction service instead dev@486: this.analyses.findIndexAndUse( dev@486: val => val.id === result.id, dev@486: (index) => this.analyses.set( dev@486: index, dev@460: Object.assign( dev@460: {}, dev@486: this.analyses.get(index), dev@460: result.result, dev@460: result.unit ? {unit: result.unit} : {} dev@460: ) dev@486: ) dev@486: ); dev@486: this.canExtract = true; dev@460: }; dev@460: return this.featureService.extract( dev@460: analysis.id, dev@460: createExtractionRequest(analysis)) dev@460: .then(findAndUpdateItem) dev@460: .catch(err => { dev@460: this.canExtract = true; dev@486: this.analyses.findIndexAndUse( dev@486: val => val.id === analysis.id, dev@486: index => this.analyses.remove(index) dev@486: ); dev@494: this.notifier.displayError(`Error whilst extracting: ${err}`); dev@460: }); dev@460: } angular-cli@0: }