# HG changeset patch # User Lucas Thompson # Date 1498815690 -3600 # Node ID ccce2c09502e56767073905946ccea22af476eb8 # Parent 8d561b6df2fab1159e3bab3f78e663dfcb556a12 Manually cherry-pick various refactoring efforts from feature/basic-session-loading diff -r 8d561b6df2fa -r ccce2c09502e src/app/Session.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/Session.ts Fri Jun 30 10:41:30 2017 +0100 @@ -0,0 +1,134 @@ +/** + * Created by lucast on 08/06/2017. + */ +import { + Item, + RootAudioItem +} from './analysis-item/AnalysisItem'; + +export const exampleSession: SerialisedNotebook = { + root: { + id: '1', + hasSharedTimeline: true, + title: 'Drum Loop', + description: 'Remotely hosted audio file', + uri: 'https://piper-audio.github.io/waves-ui-piper/examples/assets/drum-loop.wav' + }, + analyses: [ + { + id: '2', + hasSharedTimeline: true, + extractorKey: 'vamp-example-plugins:amplitudefollower', + outputId: 'amplitude', + title: 'Amplitude', + description: 'amplitude' + }, + { + id: '3', + hasSharedTimeline: true, + extractorKey: 'vamp-example-plugins:powerspectrum', + outputId: 'powerspectrum', + title: 'Simple Power Spectrum', + description: 'powerspectrum' + }, + + ] +}; + +export interface SerialisedAnalysisItem extends Item { + extractorKey: string; + outputId: string; +} + +export interface SerialisedNotebook { + root: RootAudioItem; + analyses: SerialisedAnalysisItem[]; +} + +export type ResourceRetriever = (url: string) => Promise; + +export const downloadResource: ResourceRetriever = async (url) => { + const response = await fetch(url); + const mimeType = response.headers.get('content-type'); + // Safari's fetch.blob implementation doesn't populate the type property + // causing the audio player to fail due to an unsupported type. + // Manually create a blob from an array buffer and the content type in + // the response object + const arrayBufferToBlob = async () => { + const arrayBuffer = await response.arrayBuffer(); + return new Blob([arrayBuffer], {type: mimeType}); + }; + return mimeType ? arrayBufferToBlob() : response.blob(); +}; + +export class PersistentStack { + private stack: T[]; + private history: T[][]; + + constructor() { + this.stack = []; + this.history = []; + } + + shift(): T { + this.history.push([...this.stack]); + const item = this.stack[0]; + this.stack = this.stack.slice(1); + return item; + } + + unshift(item: T): number { + this.history.push([...this.stack]); + this.stack = [item, ...this.stack]; + return this.stack.length; + } + + findIndex(predicate: (value: T, + index: number, + array: T[]) => boolean): number { + return this.stack.findIndex(predicate); + } + + filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] { + return this.stack.filter(predicate); + } + + get(index: number): T { + return this.stack[index]; + } + + set(index: number, value: T) { + this.history.push([...this.stack]); + this.stack = [ + ...this.stack.slice(0, index), + value, + ...this.stack.slice(index + 1) + ]; + } + + map(transform: (value: T, index: number, array: T[]) => U): U[] { + return this.stack.map(transform); + } + + reduce(reducer: (previousValue: U, + currentValue: T, + currentIndex: number, + array: T[]) => U, + initialValue: U): U { + return this.stack.reduce(reducer, initialValue); + } + + remove(...indices: number[]) { + this.history.push([...this.stack]); + this.stack = this.stack.reduce((acc, item, i) => { + if (!indices.includes(i)) { + acc.push(item); + } + return acc; + }, [] as T[]); + } + + toIterable(): Iterable { + return this.stack; + } +} diff -r 8d561b6df2fa -r ccce2c09502e src/app/analysis-item/AnalysisItem.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/analysis-item/AnalysisItem.ts Fri Jun 30 10:41:30 2017 +0100 @@ -0,0 +1,82 @@ +/** + * Created by lucast on 08/06/2017. + */ +import {KnownShapedFeature} from '../visualisations/FeatureUtilities'; +import {SimpleRequest} from 'piper/HigherLevelUtilities'; +export abstract class Item { + id: string; + hasSharedTimeline: boolean; + title?: string; + description?: string; + progress?: number; +} + +export interface RootAudioItem extends Item { + uri: string; + mimeType?: string; + isExportable?: boolean; +} +export interface LoadedRootAudioItem extends RootAudioItem { + audioData: AudioBuffer; +} + +export interface AnalysisItem extends Item { + parent: LoadedRootAudioItem; + extractorKey: string; + outputId: string; +} + +export type ExtractedAnalysisItem = AnalysisItem & KnownShapedFeature & { + unit?: string +}; + +export function isItem(item: Item): item is Item { + return item.id != null && item.hasSharedTimeline != null; +} + +export function isPendingRootAudioItem(item: Item): item is RootAudioItem { + return isItem(item) && typeof (item as RootAudioItem).uri === 'string'; +} + +export function isLoadedRootAudioItem(item: Item): item is LoadedRootAudioItem { + return item && isPendingRootAudioItem(item) && + (item as LoadedRootAudioItem).audioData instanceof AudioBuffer; +} + +export function isPendingAnalysisItem(item: Item): item is AnalysisItem { + const downcast = (item as ExtractedAnalysisItem); + return isLoadedRootAudioItem(downcast.parent) + && typeof downcast.extractorKey === 'string'; +} + +export function isExtractedAnalysisItem(it: Item): it is ExtractedAnalysisItem { + const downcast = (it as ExtractedAnalysisItem); + return isPendingAnalysisItem(it) && + downcast.shape != null && + downcast.collected != null; +} + +// these should probably be actual concrete types with their own getUri methods +export function getRootUri(item: Item): string { + if (isPendingRootAudioItem(item)) { + return item.uri; + } + if (isPendingAnalysisItem(item)) { + return item.parent.uri; + } + throw new Error('Invalid item: No URI property set.'); +} + +export function createExtractionRequest(item: AnalysisItem): SimpleRequest { + return { + audioData: [...Array(item.parent.audioData.numberOfChannels).keys()] + .map(i => item.parent.audioData.getChannelData(i)), + audioFormat: { + sampleRate: item.parent.audioData.sampleRate, + channelCount: item.parent.audioData.numberOfChannels, + length: item.parent.audioData.length + }, + key: item.extractorKey, + outputId: item.outputId + }; +} diff -r 8d561b6df2fa -r ccce2c09502e src/app/analysis-item/analysis-item.component.ts --- a/src/app/analysis-item/analysis-item.component.ts Fri Jun 30 00:59:38 2017 +0100 +++ b/src/app/analysis-item/analysis-item.component.ts Fri Jun 30 10:41:30 2017 +0100 @@ -14,77 +14,21 @@ import {OnSeekHandler} from '../playhead/PlayHeadHelpers'; import { defaultColourGenerator, - HigherLevelFeatureShape, - KnownShapedFeature + HigherLevelFeatureShape } from '../visualisations/FeatureUtilities'; import { RenderLoopService, TaskRemover } from '../services/render-loop/render-loop.service'; import {DomSanitizer} from '@angular/platform-browser'; - -export interface Item { - id: string; - hasSharedTimeline: boolean; - title?: string; - description?: string; - progress?: number; -} - -export interface PendingRootAudioItem extends Item { - uri: string; - mimeType?: string; - isExportable?: boolean; -} -export interface RootAudioItem extends PendingRootAudioItem { - audioData: AudioBuffer; -} - -export interface PendingAnalysisItem extends Item { - parent: RootAudioItem; - extractorKey: string; -} - -export type AnalysisItem = PendingAnalysisItem & KnownShapedFeature & { - unit?: string -}; - -export function isItem(item: Item): item is Item { - return item.id != null && item.hasSharedTimeline != null; -} - -export function isPendingRootAudioItem(item: Item): item is PendingRootAudioItem { - return isItem(item) && typeof (item as RootAudioItem).uri === 'string'; -} - -export function isRootAudioItem(item: Item): item is RootAudioItem { - return item && isPendingRootAudioItem(item) && - (item as RootAudioItem).audioData instanceof AudioBuffer; -} - -export function isPendingAnalysisItem(item: Item): item is AnalysisItem { - const downcast = (item as AnalysisItem); - return isRootAudioItem(downcast.parent) - && typeof downcast.extractorKey === 'string'; -} - -export function isAnalysisItem(item: Item): item is AnalysisItem { - const downcast = (item as AnalysisItem); - return isPendingAnalysisItem(item) && - downcast.shape != null && - downcast.collected != null; -} - -// these should probably be actual concrete types with their own getUri methods -export function getRootUri(item: Item): string { - if (isPendingRootAudioItem(item)) { - return item.uri; - } - if (isPendingAnalysisItem(item)) { - return item.parent.uri; - } - throw new Error('Invalid item: No URI property set.'); -} +import { + isExtractedAnalysisItem, + isLoadedRootAudioItem, + isPendingAnalysisItem, + isPendingRootAudioItem, + Item, + RootAudioItem +} from './AnalysisItem'; @Component({ selector: 'ugly-analysis-item', @@ -141,25 +85,25 @@ } isAudioItem(): boolean { - return this.item && isRootAudioItem(this.item); + return this.item && isLoadedRootAudioItem(this.item); } isPending(): boolean { return this.item && - !isRootAudioItem(this.item) && !isAnalysisItem(this.item) && + !isLoadedRootAudioItem(this.item) && !isExtractedAnalysisItem(this.item) && (isPendingAnalysisItem(this.item) || isPendingRootAudioItem(this.item)); } getFeatureShape(): HigherLevelFeatureShape | null { return !isPendingRootAudioItem(this.item) && - isAnalysisItem(this.item) ? this.item.shape : null; + isExtractedAnalysisItem(this.item) ? this.item.shape : null; } getDuration(): number | null { - if (isRootAudioItem(this.item)) { + if (isLoadedRootAudioItem(this.item)) { return this.item.audioData.duration; } - if (isAnalysisItem(this.item)) { + if (isExtractedAnalysisItem(this.item)) { return this.item.parent.audioData.duration; } } @@ -176,7 +120,7 @@ return this.sanitizer.bypassSecurityTrustUrl(url); } - private generateFilename(item: PendingRootAudioItem): string { + private generateFilename(item: RootAudioItem): string { // TODO this is too brittle, and will often produce the wrong result // i.e. audio/mpeg results in .mpeg, when .mp3 is likely desired const mimeParts = item.mimeType ? item.mimeType.split('/') : []; diff -r 8d561b6df2fa -r ccce2c09502e src/app/app.component.ts --- a/src/app/app.component.ts Fri Jun 30 00:59:38 2017 +0100 +++ b/src/app/app.component.ts Fri Jun 30 10:41:30 2017 +0100 @@ -4,94 +4,27 @@ AudioResourceError, AudioResource } from './services/audio-player/audio-player.service'; -import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service'; +import { + ExtractionResult, + FeatureExtractionService +} from './services/feature-extraction/feature-extraction.service'; import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component'; import {DomSanitizer} from '@angular/platform-browser'; import {MdIconRegistry} from '@angular/material'; import {Subscription} from 'rxjs/Subscription'; import { AnalysisItem, - isPendingAnalysisItem, isPendingRootAudioItem, - isRootAudioItem, + isPendingAnalysisItem, + isPendingRootAudioItem, + isLoadedRootAudioItem, Item, - PendingAnalysisItem, - PendingRootAudioItem, - RootAudioItem -} from './analysis-item/analysis-item.component'; + RootAudioItem, + LoadedRootAudioItem +} from './analysis-item/AnalysisItem'; import {OnSeekHandler} from './playhead/PlayHeadHelpers'; import {UrlResourceLifetimeManager} from './app.module'; - -class PersistentStack { - private stack: T[]; - private history: T[][]; - - constructor() { - this.stack = []; - this.history = []; - } - - shift(): T { - this.history.push([...this.stack]); - const item = this.stack[0]; - this.stack = this.stack.slice(1); - return item; - } - - unshift(item: T): number { - this.history.push([...this.stack]); - this.stack = [item, ...this.stack]; - return this.stack.length; - } - - findIndex(predicate: (value: T, - index: number, - array: T[]) => boolean): number { - return this.stack.findIndex(predicate); - } - - filter(predicate: (value: T, index: number, array: T[]) => boolean): T[] { - return this.stack.filter(predicate); - } - - get(index: number): T { - return this.stack[index]; - } - - set(index: number, value: T) { - this.history.push([...this.stack]); - this.stack = [ - ...this.stack.slice(0, index), - value, - ...this.stack.slice(index + 1) - ]; - } - - map(transform: (value: T, index: number, array: T[]) => U): U[] { - return this.stack.map(transform); - } - - reduce(reducer: (previousValue: U, - currentValue: T, - currentIndex: number, - array: T[]) => U, - initialValue: U): U { - return this.stack.reduce(reducer, initialValue); - } - - remove(...indices: number[]) { - this.history.push([...this.stack]); - this.stack = this.stack.reduce((acc, item, i) => { - if (!indices.includes(i)) { - acc.push(item); - } - return acc; - }, [] as T[]); - } - - toIterable(): Iterable { - return this.stack; - } -} +import {createExtractionRequest} from './analysis-item/AnalysisItem'; +import {PersistentStack} from './Session'; @Component({ selector: 'ugly-root', @@ -105,7 +38,7 @@ private analyses: PersistentStack; // TODO some immutable state container describing entire session private nRecordings: number; // TODO user control for naming a recording private countingId: number; // TODO improve uniquely identifying items - private rootAudioItem: RootAudioItem; + private rootAudioItem: LoadedRootAudioItem; private onSeek: OnSeekHandler; constructor(private audioService: AudioPlayerService, @@ -138,7 +71,7 @@ if (this.rootAudioItem.audioData) { this.canExtract = true; const currentRootIndex = this.analyses.findIndex(val => { - return isRootAudioItem(val) && val.uri === this.rootAudioItem.uri; + return isLoadedRootAudioItem(val) && val.uri === this.rootAudioItem.uri; }); if (currentRootIndex !== -1) { this.analyses.set( @@ -196,63 +129,34 @@ id: `${++this.countingId}`, mimeType: file.type, isExportable: createExportableItem - } as PendingRootAudioItem; - this.rootAudioItem = pending as RootAudioItem; // TODO this is silly + } as RootAudioItem; + this.rootAudioItem = pending as LoadedRootAudioItem; // TODO this is silly // TODO re-ordering of items for display // , one alternative is a Angular Pipe / Filter for use in the Template this.analyses.unshift(pending); } - extractFeatures(outputInfo: ExtractorOutputInfo): void { + extractFeatures(outputInfo: ExtractorOutputInfo): string { if (!this.canExtract || !outputInfo) { return; } this.canExtract = false; - const placeholderCard: PendingAnalysisItem = { + const placeholderCard: AnalysisItem = { parent: this.rootAudioItem, hasSharedTimeline: true, - extractorKey: outputInfo.combinedKey, + extractorKey: outputInfo.extractorKey, + outputId: outputInfo.outputId, title: outputInfo.name, description: outputInfo.outputId, id: `${++this.countingId}`, progress: 0 }; this.analyses.unshift(placeholderCard); - - const audioBuffer = this.rootAudioItem.audioData; - - this.featureService.extract(`${this.countingId}`, { - audioData: [...Array(audioBuffer.numberOfChannels).keys()] - .map(i => audioBuffer.getChannelData(i)), - audioFormat: { - sampleRate: audioBuffer.sampleRate, - channelCount: audioBuffer.numberOfChannels, - length: audioBuffer.length - }, - key: outputInfo.extractorKey, - outputId: outputInfo.outputId - }).then(result => { // TODO subscribe to the extraction service instead - const i = this.analyses.findIndex(val => val.id === result.id); - this.canExtract = true; - if (i !== -1) { - this.analyses.set( - i, - Object.assign( - {}, - this.analyses.get(i), - result.result, - result.unit ? {unit: result.unit} : {} - ) - ); - } // TODO else remove the item? - }).catch(err => { - this.canExtract = true; - this.analyses.shift(); - console.error(`Error whilst extracting: ${err}`); - }); + this.sendExtractionRequest(placeholderCard); + return placeholderCard.id; } removeItem(item: Item): void { @@ -270,13 +174,13 @@ if (this.rootAudioItem.uri === item.uri) { this.audioService.unload(); const topItem = this.analyses.get(0); - const nullRootAudio: RootAudioItem = {uri: ''} as any; // TODO eugh + const nullRootAudio: LoadedRootAudioItem = {uri: ''} as any; // TODO eugh if (topItem) { if (isPendingAnalysisItem(topItem)) { - this.rootAudioItem = topItem.parent as RootAudioItem; - } else if(isPendingRootAudioItem(topItem)) { - this.rootAudioItem = topItem as RootAudioItem + this.rootAudioItem = topItem.parent as LoadedRootAudioItem; + } else if (isPendingRootAudioItem(topItem)) { + this.rootAudioItem = topItem as LoadedRootAudioItem; } else { this.rootAudioItem = nullRootAudio; } @@ -296,4 +200,32 @@ this.onAudioDataSubscription.unsubscribe(); this.onProgressUpdated.unsubscribe(); } + + private sendExtractionRequest(analysis: AnalysisItem): Promise { + const findAndUpdateItem = (result: ExtractionResult): void => { + // TODO subscribe to the extraction service instead + const i = this.analyses.findIndex(val => val.id === result.id); + this.canExtract = true; + if (i !== -1) { + this.analyses.set( + i, + Object.assign( + {}, + this.analyses.get(i), + result.result, + result.unit ? {unit: result.unit} : {} + ) + ); + } // TODO else remove the item? + }; + return this.featureService.extract( + analysis.id, + createExtractionRequest(analysis)) + .then(findAndUpdateItem) + .catch(err => { + this.canExtract = true; + this.analyses.shift(); + console.error(`Error whilst extracting: ${err}`); + }); + } } diff -r 8d561b6df2fa -r ccce2c09502e src/app/notebook-feed/notebook-feed.component.ts --- a/src/app/notebook-feed/notebook-feed.component.ts Fri Jun 30 00:59:38 2017 +0100 +++ b/src/app/notebook-feed/notebook-feed.component.ts Fri Jun 30 10:41:30 2017 +0100 @@ -12,9 +12,9 @@ import Waves from 'waves-ui-piper'; import { getRootUri, - isRootAudioItem, + isLoadedRootAudioItem, Item -} from '../analysis-item/analysis-item.component'; +} from '../analysis-item/AnalysisItem'; import {Observable} from 'rxjs/Observable'; import {Dimension} from '../app.module'; import {Subscription} from 'rxjs/Subscription'; @@ -82,7 +82,7 @@ } isAudioItem(item: Item): boolean { - return isRootAudioItem(item); + return isLoadedRootAudioItem(item); } isActiveItem(item: Item): boolean {