import { EventEmitter } from '@angular/core'; export class AudioSequence { private buffer: AudioBuffer; private context: AudioContext; private node: AudioBufferSourceNode; private state: TrackState; private startedAt = 0; private pausedAt = 0; public onplay: EventEmitter = new EventEmitter(); public onpause: EventEmitter = new EventEmitter(); public onstop: EventEmitter = new EventEmitter(); constructor(buffer: AudioBuffer, context: AudioContext) { this.buffer = buffer; this.context = context; this.state = TrackState.STOPPED; } /** * Play the audiosequence. If the sequence was paused before it will start from the paused position * @param start optional parameter to play the sequence from a certain position (in seconds) * @param end optional parameter to play the sequence upto a certain position (in seconds) */ play(start?: number, end?: number): void { if (this.state === TrackState.PLAYING) { this.clearNode(); } start = start ? start : 0; end = end ? end : this.buffer.duration; if (this.state === TrackState.PAUSED) { start = this.pausedAt; } this.node = this.context.createBufferSource(); this.node.buffer = this.buffer; this.node.connect(this.context.destination); this.node.onended = () => { if (this.state !== TrackState.PAUSED) { this.stop(); } }; this.node.start(0, start, end - start > 0 ? end - start : 0); this.startedAt = this.context.currentTime - start; this.state = TrackState.PLAYING; this.onplay.emit(); } /** * Pause the audio sequence */ pause(): void { if (this.state !== TrackState.PLAYING) { return; } this.clearNode(); this.pausedAt = this.context.currentTime - this.startedAt; this.state = TrackState.PAUSED; this.onpause.emit(); } /** * Stop the audio sequence */ stop(): void { this.clearNode(); this.pausedAt = 0; this.startedAt = 0; this.state = TrackState.STOPPED; this.onstop.emit(); } getSection(start: number, end: number): Float32Array[] { const startIndex = this.timeToIndex(start); const endIndex = this.timeToIndex(end); const out: Float32Array[] = []; for (let i = 0; i < this.buffer.numberOfChannels; ++i) { out.push(this.buffer.getChannelData(i).subarray(startIndex, endIndex)); } return out; } /** * Insert a section into the sequence at a given position * @param at time (in seconds) to insert at * @param buffer section to insert */ insert(at: number, buffer: Float32Array[]): void { const atIndex = this.timeToIndex(at); const newLength = this.buffer.length + buffer[0].length; const newBuffer = this.context.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate); for (let i = 0; i < this.buffer.numberOfChannels; ++i) { const channelData = this.buffer.getChannelData(i); const newChannelData = new Float32Array(newLength); for (let j = 0; j < channelData.length; ++j) { if (j < atIndex) { newChannelData[j] = channelData[j]; } else if (j === atIndex) { for (let k = 0; k <= buffer[i].length; ++k) { newChannelData[j + k] = buffer[i][k]; } } else { newChannelData[j + buffer[0].length] = channelData[j]; } } newBuffer.copyToChannel(newChannelData, i, 0); } this.buffer = newBuffer; } /** * Remove a section of the audio sequence * @param start time (in seconds) for the start of the section * @param end time (in seconds) for the end of the section */ trim(start: number, end: number): void { const startIndex = Math.floor(start * this.buffer.sampleRate); const endIndex = Math.floor(end * this.buffer.sampleRate); const trimmedLength = this.buffer.length - (endIndex - startIndex); const trimmedBuffer = this.context.createBuffer(this.buffer.numberOfChannels, trimmedLength, this.buffer.sampleRate); for (let i = 0; i < this.buffer.numberOfChannels; ++i) { const channelData = this.buffer.getChannelData(i); const trimmed = new Float32Array(trimmedLength); for (let j = 0; j < trimmedLength; ++j) { if (j <= startIndex) { trimmed[j] = channelData[j]; } else { trimmed[j] = channelData[j + (endIndex - startIndex)]; } } trimmedBuffer.copyToChannel(trimmed, i, 0); } this.buffer = trimmedBuffer; } getDuration(): number { return this.buffer.duration; } getCurrentTime(): number { if (this.state === TrackState.PLAYING) { return this.context.currentTime - this.startedAt; } if (this.state === TrackState.PAUSED) { return this.pausedAt; } return 0; } setCurrentTime(time: number): void { this.stop(); this.pausedAt = time; this.state = TrackState.PAUSED; } getBuffer(): AudioBuffer { return this.buffer; } private clearNode(): void { if (this.node) { this.node.disconnect(this.context.destination); this.node.stop(0); this.node = null; this.state = TrackState.STOPPED; } } private timeToIndex(t: number): number { return Math.floor(t * this.buffer.sampleRate); } } enum TrackState { STOPPED, PLAYING, PAUSED }