@omnimedia/omnitool
Version:
open source video processing tools
134 lines (109 loc) • 3.18 kB
text/typescript
export type AudioPlanesInput = {
planes: Float32Array[]
sampleRate: number
timestamp: number
}
export type MixedChunk = {
planar: Float32Array
sampleRate: number
channels: number
frames: number
startFrame: number
}
type ActiveSample = {
startFrame: number
endFrame: number
data: Float32Array[]
}
export type AudioMixOptions = {
chunkFrames?: number
clamp?: boolean
}
export class AudioMix {
readonly #chunkFrames: number
readonly #clamp: boolean
constructor(options: AudioMixOptions = {}) {
this.#chunkFrames = options.chunkFrames ?? 1024
this.#clamp = options.clamp ?? true
}
async *mix(samples: AsyncIterable<AudioPlanesInput>): AsyncGenerator<MixedChunk> {
const chunkFrames = this.#chunkFrames
let sampleRate: number | null = null
let channels: number | null = null
const active: ActiveSample[] = []
let nextFrame = 0
let maxEnd = 0
for await (const input of samples) {
if (channels === null) {
channels = input.planes.length
sampleRate = input.sampleRate
} else {
if (input.planes.length !== channels) throw new Error(`Channel count changed`)
if (input.sampleRate !== sampleRate) throw new Error(`Sample rate changed`)
}
const inputStart = Math.round(input.timestamp * sampleRate)
const frames = input.planes[0]?.length ?? 0
while (nextFrame + chunkFrames <= inputStart) {
yield this.#processChunk(active, nextFrame, channels, sampleRate)
nextFrame += chunkFrames
}
active.push({
startFrame: inputStart,
endFrame: inputStart + frames,
data: input.planes
})
maxEnd = Math.max(maxEnd, inputStart + frames)
}
if (channels !== null && sampleRate !== null) {
while (nextFrame < maxEnd) {
yield this.#processChunk(active, nextFrame, channels, sampleRate)
nextFrame += chunkFrames
}
}
}
#processChunk(
active: ActiveSample[],
currentStartFrame: number,
channels: number,
sampleRate: number
): MixedChunk {
const chunkFrames = this.#chunkFrames
const outputBuffer = new Float32Array(channels * chunkFrames)
const chunkEnd = currentStartFrame + chunkFrames
for (let ch = 0; ch < channels; ch++) {
const channelOffset = ch * chunkFrames
const outChannelView = outputBuffer.subarray(channelOffset, channelOffset + chunkFrames)
for (const sample of active) {
const data = sample.data[ch]
if (!data) continue
const start = Math.max(currentStartFrame, sample.startFrame)
const end = Math.min(chunkEnd, sample.endFrame)
if (start >= end) continue
const dstIdx = start - currentStartFrame
const srcIdx = start - sample.startFrame
const len = end - start
for (let i = 0; i < len; i++) {
outChannelView[dstIdx + i] += data[srcIdx + i]
}
}
if (this.#clamp) {
for (let i = 0; i < chunkFrames; i++) {
const v = outChannelView[i]
outChannelView[i] = v < -1.0 ? -1.0 : (v > 1.0 ? 1.0 : v)
}
}
}
for (let i = active.length - 1; i >= 0; i--) {
if (active[i].endFrame <= chunkEnd) {
active.splice(i, 1)
}
}
return {
planar: outputBuffer,
sampleRate,
channels,
frames: chunkFrames,
startFrame: currentStartFrame
}
}
}