@echogarden/wave-codec
Version:
A fully-featured WAVE format encoder and decoder. Written in pure TypeScript.
246 lines (186 loc) • 7.2 kB
text/typescript
import { readUint32LE, writeAscii, writeUint32LE } from './utilities/BinaryUtilities.js'
import { concatUint8Arrays } from './utilities/Utilities.js'
import { decodeAscii } from './encodings/Ascii.js'
import { WaveFormatHeader, BitDepth, SampleFormat } from './WaveFormatHeader.js'
import { bufferToFloat32Channels, float32ChannelsToBuffer } from './audio-utilities/AudioBufferConversion.js'
export function encodeWaveFromFloat32Channels(
audioChannels: Float32Array[],
sampleRate: number,
bitDepth: BitDepth = 16,
sampleFormat: SampleFormat = SampleFormat.PCM,
speakerPositionMask = 0) {
const audioDataBuffer = float32ChannelsToBuffer(audioChannels, bitDepth, sampleFormat)
const resultWaveBuffer = encodeWaveFromBuffer(
audioDataBuffer,
sampleRate,
audioChannels.length,
bitDepth,
sampleFormat,
speakerPositionMask)
return resultWaveBuffer
}
export function encodeWaveFromBuffer(
audioBuffer: Uint8Array,
sampleRate: number,
channelCount: number,
bitDepth: BitDepth,
sampleFormat: SampleFormat,
speakerPositionMask: number = 0) {
// Create format subchunk
const shouldUseExtensibleFormat = bitDepth > 16 || channelCount > 2
const formatSubChunk = new WaveFormatHeader(channelCount, sampleRate, bitDepth, sampleFormat, speakerPositionMask)
const formatSubChunkBuffer = formatSubChunk.serialize(shouldUseExtensibleFormat)
// Create data subchunk
const audioBufferLength = audioBuffer.length
const dataSubChunkBuffer = new Uint8Array(4 + 4 + audioBufferLength)
writeAscii(dataSubChunkBuffer, 'data', 0)
let dataChunkLength = audioBufferLength
// Ensure large data chunk length is clipped to the maximum of 4294967295 bytes
if (dataChunkLength > 4294967295) {
dataChunkLength = 4294967295
}
writeUint32LE(dataSubChunkBuffer, dataChunkLength, 4)
dataSubChunkBuffer.set(audioBuffer, 8)
// Create RIFF subchunk
const riffChunkHeaderBuffer = new Uint8Array(12)
writeAscii(riffChunkHeaderBuffer, 'RIFF', 0)
let riffChunkLength = 4 + formatSubChunkBuffer.length + dataSubChunkBuffer.length
// Ensure large RIFF chunk length is clipped to the maximum of 4294967295 bytes
if (riffChunkLength > 4294967295) {
riffChunkLength = 4294967295
}
writeUint32LE(riffChunkHeaderBuffer, riffChunkLength, 4)
writeAscii(riffChunkHeaderBuffer, 'WAVE', 8)
// Concatenate subchunks to produce the resulting wave buffer
const resultWaveBuffer = concatUint8Arrays([riffChunkHeaderBuffer, formatSubChunkBuffer, dataSubChunkBuffer])
return resultWaveBuffer
}
export function decodeWaveToFloat32Channels(
waveData: Uint8Array,
ignoreTruncatedChunks = true,
ignoreOverflowingDataChunks = true) {
const {
decodedAudioBuffer,
sampleRate,
channelCount,
bitDepth,
sampleFormat,
speakerPositionMask
} = decodeWaveToBuffer(waveData, ignoreTruncatedChunks, ignoreOverflowingDataChunks)
const audioChannels = bufferToFloat32Channels(
decodedAudioBuffer,
channelCount,
bitDepth,
sampleFormat)
return {
audioChannels,
sampleRate
} as RawAudio
}
export function decodeWaveToBuffer(
waveData: Uint8Array,
ignoreTruncatedChunks = true,
ignoreOverflowingDataChunks = true) {
let readOffset = 0
const riffId = decodeAscii(waveData.subarray(readOffset, readOffset + 4))
if (riffId !== 'RIFF') {
throw new Error('Not a valid wave file. No RIFF id found at offset 0.')
}
readOffset += 4
let riffChunkSize = readUint32LE(waveData, readOffset)
readOffset += 4
const waveId = decodeAscii(waveData.subarray(readOffset, readOffset + 4))
if (waveId !== 'WAVE') {
throw new Error('Not a valid wave file. No WAVE id found at offset 8.')
}
if (ignoreOverflowingDataChunks && riffChunkSize === 4294967295) {
riffChunkSize = waveData.length - 8
}
if (riffChunkSize < waveData.length - 8) {
throw new Error(`RIFF chunk length ${riffChunkSize} is smaller than the remaining size of the buffer (${waveData.length - 8})`)
}
if (!ignoreTruncatedChunks && riffChunkSize > waveData.length - 8) {
throw new Error(`RIFF chunk length (${riffChunkSize}) is greater than the remaining size of the buffer (${waveData.length - 8})`)
}
readOffset += 4
let formatSubChunkBodyBuffer: Uint8Array | undefined
const dataBuffers: Uint8Array[] = []
while (true) {
const subChunkIdentifier = decodeAscii(waveData.subarray(readOffset, readOffset + 4))
readOffset += 4
let subChunkSize = readUint32LE(waveData, readOffset)
readOffset += 4
if (!ignoreTruncatedChunks && subChunkSize > waveData.length - readOffset) {
throw new Error(`Encountered a '${subChunkIdentifier}' subchunk with a size of ${subChunkSize} which is greater than the remaining size of the buffer (${waveData.length - readOffset})`)
}
if (subChunkIdentifier === 'fmt ') {
formatSubChunkBodyBuffer = waveData.subarray(readOffset, readOffset + subChunkSize)
} else if (subChunkIdentifier === 'data') {
if (!formatSubChunkBodyBuffer) {
throw new Error('A data subchunk was encountered before a format subchunk')
}
// If the data chunk is truncated or extended beyond 4 GiB,
// the data would be read up to the end of the buffer
if (ignoreOverflowingDataChunks && subChunkSize === 4294967295) {
subChunkSize = waveData.length - readOffset
}
const subChunkData = waveData.subarray(readOffset, readOffset + subChunkSize)
dataBuffers.push(subChunkData)
}
// All sub chunks other than 'fmt ' and 'data' (e.g. 'LIST', 'fact', 'plst', 'junk' etc.) are ignored
// This addition operation may overflow if JavaScript integers were 32 bits,
// but since they are 52 bits, it is okay:
readOffset += subChunkSize
// Break if readOffset is equal to or is greater than the size of the buffer
if (readOffset >= waveData.length) {
break
}
}
if (!formatSubChunkBodyBuffer) {
throw new Error('No format subchunk was found in the wave file')
}
if (dataBuffers.length === 0) {
throw new Error('No data subchunks were found in the wave file')
}
const waveFormat = WaveFormatHeader.deserializeFrom(formatSubChunkBodyBuffer)
const sampleFormat = waveFormat.sampleFormat
const channelCount = waveFormat.channelCount
const sampleRate = waveFormat.sampleRate
const bitDepth = waveFormat.bitDepth
const speakerPositionMask = waveFormat.speakerPositionMask
// Note: the returned audio buffer must be ensured to be
// memory aligned to the target bit depth, like 2 bytes or 4 bytes for 16 and 32 bits.
const decodedAudioBuffer = concatUint8Arrays(dataBuffers)
return {
decodedAudioBuffer,
sampleRate,
channelCount,
bitDepth,
sampleFormat,
speakerPositionMask
}
}
export function repairWaveData(waveData: Uint8Array) {
const {
decodedAudioBuffer,
sampleRate,
channelCount,
bitDepth,
sampleFormat,
speakerPositionMask
} = decodeWaveToBuffer(waveData, true, true)
const reEncodedWaveData = encodeWaveFromBuffer(
decodedAudioBuffer,
sampleRate,
channelCount,
bitDepth,
sampleFormat,
speakerPositionMask)
return reEncodedWaveData
}
export * from './audio-utilities/AudioBufferConversion.js'
export { type BitDepth, SampleFormat } from './WaveFormatHeader.js'
export type RawAudio = {
audioChannels: Float32Array[]
sampleRate: number
}