@pompeii-labs/audio
Version:
The Audio SDK from Pompeii Labs
555 lines (545 loc) • 10.3 kB
JavaScript
// src/decoders/wav.ts
function decodeWAV(bytes) {
const view = new DataView(bytes.buffer);
if (String.fromCharCode(...bytes.slice(0, 4)) !== "RIFF" || String.fromCharCode(...bytes.slice(8, 12)) !== "WAVE") {
console.log("Invalid WAV header detected");
return null;
}
let offset = 12;
let audioFormat = 0;
let channels = 0;
let sampleRate = 0;
let bitsPerSample = 0;
let dataOffset = 0;
let dataSize = 0;
while (offset < bytes.length - 8) {
const chunkId = String.fromCharCode(...bytes.slice(offset, offset + 4));
const chunkSize = view.getUint32(offset + 4, true);
if (chunkId === "fmt ") {
audioFormat = view.getUint16(offset + 8, true);
channels = view.getUint16(offset + 10, true);
sampleRate = view.getUint32(offset + 12, true);
bitsPerSample = view.getUint16(offset + 22, true);
} else if (chunkId === "data") {
dataOffset = offset + 8;
const availableSize = bytes.length - dataOffset;
dataSize = Math.min(chunkSize, availableSize);
break;
}
offset += 8 + chunkSize;
}
if (audioFormat !== 1) {
throw new Error(`Unsupported WAV format: ${audioFormat}`);
}
const bytesPerSample = bitsPerSample / 8;
const numSamples = Math.floor(dataSize / bytesPerSample);
const samples = new Float32Array(numSamples);
for (let i = 0; i < numSamples; i++) {
const byteOffset = dataOffset + i * bytesPerSample;
if (byteOffset + bytesPerSample > bytes.length) {
console.error("Buffer overflow detected:", {
byteOffset,
bytesPerSample,
bufferLength: bytes.length,
sampleIndex: i
});
throw new Error("Buffer overflow while reading samples");
}
let sample = 0;
if (bitsPerSample === 8) {
sample = (bytes[byteOffset] - 128) / 128;
} else if (bitsPerSample === 16) {
const rawValue = view.getInt16(byteOffset, true);
sample = rawValue / 32768;
} else if (bitsPerSample === 24) {
const byte1 = bytes[byteOffset];
const byte2 = bytes[byteOffset + 1];
const byte3 = bytes[byteOffset + 2];
const value = byte3 << 16 | byte2 << 8 | byte1;
sample = (value > 8388607 ? value - 16777216 : value) / 8388608;
} else if (bitsPerSample === 32) {
sample = view.getFloat32(byteOffset, true);
}
samples[i] = Math.max(-1, Math.min(1, sample));
}
const result = {
sampleRate,
channels,
bitsPerSample,
samples,
duration: numSamples / (sampleRate * channels)
// Calculate duration in seconds
};
return result;
}
// src/decoders/mulaw.ts
function mulawToPcm16(mulawData) {
const pcmData = new Int16Array(mulawData.length);
for (let i = 0; i < mulawData.length; i++) {
pcmData[i] = mulawToLinear(mulawData[i]);
}
return pcmData;
}
function mulawToLinear(mulawByte) {
const inverted = mulawByte ^ 255;
const sign = inverted & 128;
const segment = (inverted & 112) >> 4;
const step = inverted & 15;
let linear;
if (segment === 0) {
linear = (step << 1) + 1;
} else {
linear = (step << 1) + 1 + 32 << segment + 2;
}
linear -= 33;
return sign ? -linear : linear;
}
// src/encoders/mulaw.ts
var BIAS = 132;
var CLIP = 32635;
var encodeTable = [
0,
0,
1,
1,
2,
2,
2,
2,
3,
3,
3,
3,
3,
3,
3,
3,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
6,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7,
7
];
function encodeSample(sample) {
const sign = sample >> 8 & 128;
if (sign !== 0) sample = -sample;
sample = sample + BIAS;
if (sample > CLIP) sample = CLIP;
const exponent = encodeTable[sample >> 7 & 255];
const mantissa = sample >> exponent + 3 & 15;
return ~(sign | exponent << 4 | mantissa);
}
function pcm16ToMulaw(pcmData) {
const mulawData = new Uint8Array(pcmData.length);
for (let i = 0; i < pcmData.length; i++) {
mulawData[i] = encodeSample(pcmData[i]);
}
return mulawData;
}
// src/helpers/bufferToInt16Array.ts
function bufferToInt16Array(buffer) {
return new Int16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2);
}
// src/helpers/int16ArrayToBuffer.ts
function int16ArrayToBuffer(int16Array) {
return Buffer.from(int16Array.buffer, int16Array.byteOffset, int16Array.byteLength);
}
// src/helpers/convertAudioFormat.ts
function encodePcm(audio, encoding) {
switch (encoding) {
case "mulaw":
return Buffer.from(pcm16ToMulaw(audio));
case "pcm":
return int16ArrayToBuffer(audio);
default:
throw new Error(`Could not encode audio: Unsupported encoding: ${encoding}`);
}
}
function decodeToPcm(audio, encoding) {
switch (encoding) {
case "mulaw":
return mulawToPcm16(audio);
case "pcm":
return bufferToInt16Array(audio);
default:
throw new Error(`Could not decode audio: Unsupported encoding: ${encoding}`);
}
}
// src/helpers/detectFormat.ts
function identifyAudioFormat(bytes) {
const checkBytes = (offset, expected) => {
if (offset + expected.length > bytes.length) return false;
return expected.every((byte, i) => bytes[offset + i] === byte);
};
if (checkBytes(0, [82, 73, 70, 70]) && // "RIFF"
checkBytes(8, [87, 65, 86, 69])) {
return {
format: "WAV",
mimeType: "audio/wav",
description: "Waveform Audio File Format"
};
}
if (checkBytes(0, [73, 68, 51]) || // ID3 tag
checkBytes(0, [255, 251]) || // MP3 frame sync
checkBytes(0, [255, 243]) || // MP3 frame sync
checkBytes(0, [255, 242])) {
return {
format: "MP3",
mimeType: "audio/mpeg",
description: "MPEG Audio Layer III"
};
}
if (checkBytes(0, [102, 76, 97, 67])) {
return {
format: "FLAC",
mimeType: "audio/flac",
description: "Free Lossless Audio Codec"
};
}
if (checkBytes(0, [79, 103, 103, 83])) {
return {
format: "OGG",
mimeType: "audio/ogg",
description: "Ogg Vorbis"
};
}
if (checkBytes(4, [102, 116, 121, 112]) && // "ftyp"
(checkBytes(8, [77, 52, 65, 32]) || // "M4A "
checkBytes(8, [105, 115, 111, 109]))) {
return {
format: "M4A",
mimeType: "audio/mp4",
description: "MPEG-4 Audio"
};
}
if (checkBytes(0, [70, 79, 82, 77]) && // "FORM"
checkBytes(8, [65, 73, 70, 70])) {
return {
format: "AIFF",
mimeType: "audio/aiff",
description: "Audio Interchange File Format"
};
}
if (checkBytes(
0,
[
48,
38,
178,
117,
142,
102,
207,
17,
166,
217,
0,
170,
0,
98,
206,
108
]
)) {
return {
format: "WMA",
mimeType: "audio/x-ms-wma",
description: "Windows Media Audio"
};
}
return null;
}
// src/helpers/generateFadeOutSamples.ts
function generateFadeOutSamples(lastSampleValue, fadeDurationMs, sampleRate) {
const fadeNumSamples = Math.ceil(fadeDurationMs / 1e3 * sampleRate);
const fadeSamples = new Int16Array(fadeNumSamples);
for (let i = 0; i < fadeNumSamples; i++) {
const progress = 1 - i / (fadeNumSamples - 1);
fadeSamples[i] = Math.round(lastSampleValue * progress);
}
return new Uint8Array(fadeSamples.buffer);
}
// src/helpers/resamplePcm.ts
function resamplePcm(pcm, originalSampleRate, targetSampleRate) {
if (originalSampleRate === targetSampleRate) {
return pcm;
}
const ratio = originalSampleRate / targetSampleRate;
const newLength = Math.floor(pcm.length / ratio);
const newSamples = new Int16Array(newLength);
if (ratio < 1) {
for (let i = 0; i < newSamples.length; i++) {
const exactPos = i * ratio;
const lowerIndex = Math.floor(exactPos);
const upperIndex = Math.min(lowerIndex + 1, pcm.length - 1);
const fraction = exactPos - lowerIndex;
const lowerSample = pcm[lowerIndex];
const upperSample = pcm[upperIndex];
newSamples[i] = Math.round(lowerSample + (upperSample - lowerSample) * fraction);
}
return newSamples;
}
const nyquistFreq = targetSampleRate / 2;
const cutoffFreq = nyquistFreq * 0.9;
const filteredPcm = applyLowPassFilter(pcm, originalSampleRate, cutoffFreq);
for (let i = 0; i < newSamples.length; i++) {
const exactPos = i * ratio;
const lowerIndex = Math.floor(exactPos);
const upperIndex = Math.min(lowerIndex + 1, filteredPcm.length - 1);
const fraction = exactPos - lowerIndex;
const lowerSample = filteredPcm[lowerIndex];
const upperSample = filteredPcm[upperIndex];
newSamples[i] = Math.round(lowerSample + (upperSample - lowerSample) * fraction);
}
return newSamples;
}
function applyLowPassFilter(pcm, sampleRate, cutoffFreq) {
const filterOrder = Math.max(3, Math.floor(sampleRate / (cutoffFreq * 4)));
const filtered = new Int16Array(pcm.length);
for (let i = 0; i < pcm.length; i++) {
let sum = 0;
let count = 0;
for (let j = Math.max(0, i - filterOrder); j <= Math.min(pcm.length - 1, i + filterOrder); j++) {
sum += pcm[j];
count++;
}
filtered[i] = Math.round(sum / count);
}
return filtered;
}
export { bufferToInt16Array, decodeToPcm, decodeWAV, encodePcm, generateFadeOutSamples, identifyAudioFormat, int16ArrayToBuffer, mulawToPcm16, pcm16ToMulaw, resamplePcm };