{"version":3,"sources":["src/common.browser/FileAudioSource.ts"],"names":[],"mappings":";AAGA,OAAO,EAEH,wBAAwB,EAE3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAInB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAqB,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE7F,qBAAa,eAAgB,YAAW,YAAY;IAEhD,OAAO,CAAC,sBAAsB,CAAiC;IAE/D,OAAO,CAAC,WAAW,CAA8C;IAEjE,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,UAAU,CAAgC;IAElD,OAAO,CAAC,UAAU,CAAgB;IAElC,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO,CAAC,aAAa,CAAc;gBAEhB,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM;IAcjF,IAAW,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAElD;IAEM,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvB,EAAE,IAAI,MAAM;IAIN,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAkB5D,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc/B,IAAW,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAEjD;IAED,IAAW,UAAU,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAmBzD;IAED,OAAO,CAAC,UAAU;YAyDJ,MAAM;IAkDpB,OAAO,CAAC,OAAO;CAIlB","file":"FileAudioSource.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport {\r\n    connectivity,\r\n    ISpeechConfigAudioDevice,\r\n    type,\r\n} from \"../common.speech/Exports.js\";\r\nimport {\r\n    AudioSourceErrorEvent,\r\n    AudioSourceEvent,\r\n    AudioSourceInitializingEvent,\r\n    AudioSourceOffEvent,\r\n    AudioSourceReadyEvent,\r\n    AudioStreamNodeAttachedEvent,\r\n    AudioStreamNodeAttachingEvent,\r\n    AudioStreamNodeDetachedEvent,\r\n    AudioStreamNodeErrorEvent,\r\n    ChunkedArrayBufferStream,\r\n    createNoDashGuid,\r\n    Deferred,\r\n    Events,\r\n    EventSource,\r\n    IAudioSource,\r\n    IAudioStreamNode,\r\n    IStreamChunk,\r\n    IStringDictionary,\r\n    Stream,\r\n} from \"../common/Exports.js\";\r\nimport { AudioStreamFormat, AudioStreamFormatImpl } from \"../sdk/Audio/AudioStreamFormat.js\";\r\n\r\nexport class FileAudioSource implements IAudioSource {\r\n\r\n    private privAudioFormatPromise: Promise<AudioStreamFormatImpl>;\r\n\r\n    private privStreams: IStringDictionary<Stream<ArrayBuffer>> = {};\r\n\r\n    private privId: string;\r\n\r\n    private privEvents: EventSource<AudioSourceEvent>;\r\n\r\n    private privSource: Blob | Buffer;\r\n\r\n    private privFilename: string;\r\n\r\n    private privHeaderEnd: number = 44;\r\n\r\n    public constructor(file: File | Buffer, filename?: string, audioSourceId?: string) {\r\n        this.privId = audioSourceId ? audioSourceId : createNoDashGuid();\r\n        this.privEvents = new EventSource<AudioSourceEvent>();\r\n        this.privSource = file;\r\n        if (typeof window !== \"undefined\" && typeof Blob !== \"undefined\" && this.privSource instanceof Blob) {\r\n            this.privFilename = (file as File).name;\r\n        } else {\r\n            this.privFilename = filename || \"unknown.wav\";\r\n        }\r\n\r\n        // Read the header.\r\n        this.privAudioFormatPromise = this.readHeader();\r\n    }\r\n\r\n    public get format(): Promise<AudioStreamFormatImpl> {\r\n        return this.privAudioFormatPromise;\r\n    }\r\n\r\n    public turnOn(): Promise<void> {\r\n        if (this.privFilename.lastIndexOf(\".wav\") !== this.privFilename.length - 4) {\r\n            const errorMsg = this.privFilename + \" is not supported. Only WAVE files are allowed at the moment.\";\r\n            this.onEvent(new AudioSourceErrorEvent(errorMsg, \"\"));\r\n            return Promise.reject(errorMsg);\r\n        }\r\n\r\n        this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id\r\n        this.onEvent(new AudioSourceReadyEvent(this.privId));\r\n        return;\r\n    }\r\n\r\n    public id(): string {\r\n        return this.privId;\r\n    }\r\n\r\n    public async attach(audioNodeId: string): Promise<IAudioStreamNode> {\r\n        this.onEvent(new AudioStreamNodeAttachingEvent(this.privId, audioNodeId));\r\n\r\n        const stream: Stream<ArrayBuffer> = await this.upload(audioNodeId);\r\n\r\n        this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\r\n        return Promise.resolve({\r\n            detach: async (): Promise<void> => {\r\n                stream.readEnded();\r\n                delete this.privStreams[audioNodeId];\r\n                this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\r\n                await this.turnOff();\r\n            },\r\n            id: (): string => audioNodeId,\r\n            read: (): Promise<IStreamChunk<ArrayBuffer>> => stream.read(),\r\n        });\r\n    }\r\n\r\n    public detach(audioNodeId: string): void {\r\n        if (audioNodeId && this.privStreams[audioNodeId]) {\r\n            this.privStreams[audioNodeId].close();\r\n            delete this.privStreams[audioNodeId];\r\n            this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\r\n        }\r\n    }\r\n\r\n    public turnOff(): Promise<void> {\r\n        for (const streamId in this.privStreams) {\r\n            if (streamId) {\r\n                const stream = this.privStreams[streamId];\r\n                if (stream && !stream.isClosed) {\r\n                    stream.close();\r\n                }\r\n            }\r\n        }\r\n\r\n        this.onEvent(new AudioSourceOffEvent(this.privId)); // no stream now\r\n        return Promise.resolve();\r\n    }\r\n\r\n    public get events(): EventSource<AudioSourceEvent> {\r\n        return this.privEvents;\r\n    }\r\n\r\n    public get deviceInfo(): Promise<ISpeechConfigAudioDevice> {\r\n        return this.privAudioFormatPromise.then<ISpeechConfigAudioDevice>((result: AudioStreamFormatImpl): Promise<{\r\n            bitspersample: number;\r\n            channelcount: number;\r\n            connectivity: connectivity.Unknown;\r\n            manufacturer: string;\r\n            model: string;\r\n            samplerate: number;\r\n            type: type.File;\r\n        }> => ( Promise.resolve({\r\n                bitspersample: result.bitsPerSample,\r\n                channelcount: result.channels,\r\n                connectivity: connectivity.Unknown,\r\n                manufacturer: \"Speech SDK\",\r\n                model: \"File\",\r\n                samplerate: result.samplesPerSec,\r\n                type: type.File,\r\n            })\r\n        ));\r\n    }\r\n\r\n    private readHeader(): Promise<AudioStreamFormatImpl> {\r\n        // Read the wave header.\r\n        const maxHeaderSize: number = 4296;\r\n        const header: Blob | Buffer = this.privSource.slice(0, maxHeaderSize);\r\n\r\n        const headerResult: Deferred<AudioStreamFormatImpl> = new Deferred<AudioStreamFormatImpl>();\r\n\r\n        const processHeader = (header: ArrayBuffer): void => {\r\n            const view: DataView = new DataView(header);\r\n\r\n            const getWord = (index: number): string => String.fromCharCode(view.getUint8(index), view.getUint8(index + 1), view.getUint8(index + 2), view.getUint8(index + 3));\r\n\r\n            // RIFF 4 bytes.\r\n            if (\"RIFF\" !== getWord(0)) {\r\n                headerResult.reject(\"Invalid WAV header in file, RIFF was not found\");\r\n                return;\r\n            }\r\n\r\n            // length, 4 bytes\r\n            // RIFF Type & fmt 8 bytes\r\n            if (\"WAVE\" !== getWord(8) || \"fmt \" !== getWord(12)) {\r\n                headerResult.reject(\"Invalid WAV header in file, WAVEfmt was not found\");\r\n                return;\r\n            }\r\n\r\n            const formatSize: number = view.getInt32(16, true);\r\n            const channelCount: number = view.getUint16(22, true);\r\n            const sampleRate: number = view.getUint32(24, true);\r\n            const bitsPerSample: number = view.getUint16(34, true);\r\n            // Confirm if header is 44 bytes long.\r\n            let pos: number = 36 + Math.max(formatSize - 16, 0);\r\n            for (; getWord(pos) !== \"data\"; pos += 2) {\r\n                if (pos > maxHeaderSize - 8) {\r\n                    headerResult.reject(\"Invalid WAV header in file, data block was not found\");\r\n                    return;\r\n                }\r\n            }\r\n            this.privHeaderEnd = pos + 8;\r\n            headerResult.resolve(AudioStreamFormat.getWaveFormatPCM(sampleRate, bitsPerSample, channelCount) as AudioStreamFormatImpl);\r\n        };\r\n\r\n        if (typeof window !== \"undefined\" && typeof Blob !== \"undefined\" && header instanceof Blob) {\r\n            const reader: FileReader = new FileReader();\r\n\r\n            reader.onload = (event: Event): void => {\r\n                const header: ArrayBuffer = (event.target as FileReader).result as ArrayBuffer;\r\n                processHeader(header);\r\n            };\r\n\r\n            reader.readAsArrayBuffer(header);\r\n        } else {\r\n            const h: Buffer = header as Buffer;\r\n            processHeader(h.buffer.slice(h.byteOffset, h.byteOffset + h.byteLength));\r\n        }\r\n        return headerResult.promise;\r\n    }\r\n\r\n    private async upload(audioNodeId: string): Promise<Stream<ArrayBuffer>> {\r\n        const onerror = (error: string): void => {\r\n            const errorMsg = `Error occurred while processing '${this.privFilename}'. ${error}`;\r\n            this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, errorMsg));\r\n            throw new Error(errorMsg);\r\n        };\r\n\r\n        try {\r\n            await this.turnOn();\r\n\r\n            const format: AudioStreamFormatImpl = await this.privAudioFormatPromise;\r\n            const stream = new ChunkedArrayBufferStream(format.avgBytesPerSec / 10, audioNodeId);\r\n\r\n            this.privStreams[audioNodeId] = stream;\r\n            const chunk: Blob | Buffer = this.privSource.slice(this.privHeaderEnd);\r\n\r\n            const processFile = (buff: ArrayBuffer): void => {\r\n                if (stream.isClosed) {\r\n                    return; // output stream was closed (somebody called TurnOff). We're done here.\r\n                }\r\n\r\n                stream.writeStreamChunk({\r\n                    buffer: buff,\r\n                    isEnd: false,\r\n                    timeReceived: Date.now(),\r\n                });\r\n                stream.close();\r\n            };\r\n\r\n            if (typeof window !== \"undefined\" && typeof Blob !== \"undefined\" && chunk instanceof Blob) {\r\n                const reader: FileReader = new FileReader();\r\n                reader.onerror = (ev: ProgressEvent<FileReader>): void  =>  onerror(ev.toString());\r\n\r\n                reader.onload = (event: Event): void => {\r\n                    const fileBuffer: ArrayBuffer = (event.target as FileReader).result as ArrayBuffer;\r\n                    processFile(fileBuffer);\r\n                };\r\n\r\n                reader.readAsArrayBuffer(chunk);\r\n            } else {\r\n                const c: Buffer = chunk as Buffer;\r\n                processFile(c.buffer.slice(c.byteOffset, c.byteOffset + c.byteLength));\r\n            }\r\n\r\n            return stream;\r\n        } catch (e) {\r\n            onerror(e as string);\r\n        }\r\n    }\r\n\r\n    private onEvent(event: AudioSourceEvent): void {\r\n        this.privEvents.onEvent(event);\r\n        Events.instance.onEvent(event);\r\n    }\r\n}\r\n"]}