portkey-ai
Version:
Node client library for the Portkey API
447 lines (376 loc) • 12.4 kB
text/typescript
import fs from 'fs';
import { promisify } from 'util';
const open = promisify(fs.open);
const read = promisify(fs.read);
const stat = promisify(fs.stat);
const close = promisify(fs.close);
/**
* Get audio file duration in milliseconds
* Uses optimized file reading to avoid loading entire files into memory
*/
async function getAudioFileDuration(filePath: string): Promise<string | null> {
const extension = filePath.split('.').pop()?.toLowerCase();
const fileStats = await stat(filePath);
const fileSize = fileStats.size;
switch (extension) {
case 'wav':
return calculateWavDuration(filePath);
case 'mp3':
return calculateMp3Duration(filePath, fileSize);
case 'mpga':
return calculateMpgaDuration(filePath, fileSize);
case 'flac':
return calculateFlacDuration(filePath);
case 'ogg':
return calculateOggDuration(filePath, fileSize);
case 'mp4':
case 'm4a':
return calculateMp4Duration(filePath);
default:
return null;
}
}
/**
* Helper function to read a portion of a file
*/
async function readFileChunk(
filePath: string,
position: number,
length: number
): Promise<Buffer> {
const fd = await open(filePath, 'r');
const buffer = Buffer.alloc(length);
try {
const { bytesRead } = await read(fd, buffer, 0, length, position);
if (bytesRead < length) {
return buffer.slice(0, bytesRead);
}
return buffer;
} finally {
await close(fd);
}
}
async function calculateWavDuration(filePath: string): Promise<string | null> {
try {
// For WAV files, we only need to read the header (44 bytes is sufficient)
const buffer = await readFileChunk(filePath, 0, 44);
// Check if buffer has enough bytes for WAV header
if (buffer.length < 44) {
return null;
}
const sampleRate = buffer.readUInt32LE(24);
const dataSize = buffer.readUInt32LE(40);
const bitsPerSample = buffer.readUInt16LE(34);
const channels = buffer.readUInt16LE(22);
const durationSec =
dataSize / (sampleRate * channels * (bitsPerSample / 8));
const durationMs = Math.round(durationSec * 1000);
return durationMs.toString();
} catch (error) {
return null;
}
}
async function calculateMp3Duration(
filePath: string,
fileSize: number
): Promise<string | null> {
try {
// Read the first 10KB to find the header
const headerBuffer = (await readFileChunk(
filePath,
0,
Math.min(10240, fileSize)
)) as any;
if (headerBuffer.length < 128) {
return null;
}
for (let i = 0; i < headerBuffer.length - 4; i++) {
if (headerBuffer[i] === 0xff && (headerBuffer[i + 1] & 0xe0) === 0xe0) {
const bitrateIndex = (headerBuffer[i + 2] >> 4) & 0x0f;
const sampleRateIndex = (headerBuffer[i + 2] >> 2) & 0x03;
const bitrates = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
];
const sampleRates = [44100, 48000, 32000];
// Check for valid indices
if (
bitrateIndex >= bitrates.length ||
sampleRateIndex >= sampleRates.length
) {
continue;
}
// Now we know these indices are valid
const bitrate = Number(bitrates[bitrateIndex]);
const sampleRate = Number(sampleRates[sampleRateIndex]);
if (!bitrate || !sampleRate) {
continue;
}
const fileSizeInBits = fileSize * 8;
const duration = (fileSizeInBits / (bitrate * 1000)) * 1000;
return duration.toFixed(0);
}
}
} catch (error) {
// If any calculation fails, return null
}
return null;
}
async function calculateMpgaDuration(
filePath: string,
fileSize: number
): Promise<string | null> {
try {
// Read the first 10KB to find the header
const headerBuffer = (await readFileChunk(
filePath,
0,
Math.min(10240, fileSize)
)) as any;
if (headerBuffer.length < 128) {
return null;
}
for (let i = 0; i < headerBuffer.length - 4; i++) {
if (headerBuffer[i] === 0xff && (headerBuffer[i + 1] & 0xf0) === 0xf0) {
const bitrateIndex = (headerBuffer[i + 2] >> 4) & 0x0f;
const sampleRateIndex = (headerBuffer[i + 2] >> 2) & 0x03;
const bitrates = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
];
const sampleRates = [
[44100, 48000, 32000, 0],
[22050, 24000, 16000, 0],
[11025, 12000, 8000, 0],
] as any;
const versionBits = (headerBuffer[i + 1] >> 3) & 0x03;
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
// Check all indices are valid
if (
bitrateIndex === 0 ||
bitrateIndex >= bitrates.length ||
sampleRateIndex >= 3 ||
versionIndex >= sampleRates.length
) {
continue;
}
// Check for valid sampleRate index
if (sampleRateIndex >= sampleRates[versionIndex]?.length) {
continue;
}
// Now we know these indices are valid
const bitrate = Number(bitrates[bitrateIndex]);
const sampleRate = Number(sampleRates[versionIndex]?.[sampleRateIndex]);
if (!bitrate || !sampleRate) {
continue;
}
const fileSizeInBits = fileSize * 8;
const duration = (fileSizeInBits / (bitrate * 1000)) * 1000;
return duration.toFixed(0);
}
}
} catch (error) {
// If any calculation fails, return null
}
return null;
}
async function calculateFlacDuration(filePath: string): Promise<string | null> {
try {
// Read the first portion of the file to find FLAC metadata
const headerBuffer = await readFileChunk(filePath, 0, 4);
if (headerBuffer.toString() !== 'fLaC') {
return null;
}
// Read up to 100KB to find the stream info block
const metadataBuffer = (await readFileChunk(filePath, 4, 102400)) as any;
let offset = 0;
let isLastBlock = false;
let foundStreamInfo = false;
let totalSamples = 0;
let sampleRate = 0;
while (!isLastBlock && offset < metadataBuffer.length) {
if (offset + 4 > metadataBuffer.length) {
break;
}
isLastBlock = (metadataBuffer[offset] & 0x80) === 0x80;
const blockType = metadataBuffer[offset] & 0x7f;
const blockLength = metadataBuffer.readUInt32BE(offset) & 0x00ffffff;
offset += 4;
if (blockType === 0 && offset + 18 <= metadataBuffer.length) {
sampleRate =
(metadataBuffer[offset + 10] << 12) |
(metadataBuffer[offset + 11] << 4) |
((metadataBuffer[offset + 12] & 0xf0) >> 4);
totalSamples =
((metadataBuffer[offset + 13] & 0x0f) << 32) |
(metadataBuffer[offset + 14] << 24) |
(metadataBuffer[offset + 15] << 16) |
(metadataBuffer[offset + 16] << 8) |
metadataBuffer[offset + 17];
foundStreamInfo = true;
break;
}
offset += blockLength;
}
if (!foundStreamInfo || sampleRate === 0) {
return null;
}
const durationSec = totalSamples / sampleRate;
const durationMs = Math.round(durationSec * 1000);
return durationMs.toString();
} catch (error) {
return null;
}
}
async function calculateOggDuration(
filePath: string,
fileSize: number
): Promise<string | null> {
try {
// Read the beginning of the file to check signature
const headerBuffer = await readFileChunk(filePath, 0, 4);
if (headerBuffer.toString() !== 'OggS') {
return null;
}
// For Ogg, we need to read the beginning for the sample rate and end for granule position
const headPart = await readFileChunk(
filePath,
0,
Math.min(10000, fileSize)
);
// Read the last 10KB of the file to find the last page
const tailSize = Math.min(10240, fileSize);
const tailPart = await readFileChunk(
filePath,
Math.max(0, fileSize - tailSize),
tailSize
);
let sampleRate = 0;
let offset = 0;
// Find sample rate in the header part
while (offset < headPart.length - 7) {
if (
offset + 7 <= headPart.length &&
headPart.slice(offset, offset + 7).toString() === '\x01vorbis'
) {
if (offset + 16 < headPart.length) {
sampleRate = headPart.readUInt32LE(offset + 12);
break;
}
}
offset++;
}
if (sampleRate === 0) {
return null;
}
// Find the last granule position in the tail part
let lastGranulePos = 0;
offset = 0;
while (offset < tailPart.length - 27) {
if (
offset + 4 <= tailPart.length &&
tailPart.slice(offset, offset + 4).toString() === 'OggS'
) {
if (offset + 14 < tailPart.length) {
const granulePos = Number(tailPart.readBigInt64LE(offset + 6));
if (granulePos > lastGranulePos) {
lastGranulePos = granulePos;
}
if (offset + 27 >= tailPart.length) {
break;
}
const pageSegments = tailPart[offset + 26] as any;
const headerSize = 27 + pageSegments;
let pageSize = headerSize;
for (
let i = 0;
i < pageSegments && offset + 27 + i < tailPart.length;
i++
) {
pageSize += tailPart[offset + 27 + i];
}
offset += pageSize;
} else {
break;
}
} else {
offset++;
}
}
if (lastGranulePos === 0) {
return null;
}
const durationSec = lastGranulePos / sampleRate;
const durationMs = Math.round(durationSec * 1000);
return durationMs.toString();
} catch (error) {
return null;
}
}
async function calculateMp4Duration(filePath: string): Promise<string | null> {
try {
const fd = await open(filePath, 'r');
try {
return await findMp4Duration(fd, filePath, 0);
} finally {
await close(fd);
}
} catch (error) {
return null;
}
}
async function findMp4Duration(
fd: number,
filePath: string,
position: number
): Promise<string | null> {
try {
const headerSize = 8;
const headerBuffer = Buffer.alloc(headerSize);
const { bytesRead } = await read(fd, headerBuffer, 0, headerSize, position);
if (bytesRead < headerSize) return null;
const atomSize = headerBuffer.readUInt32BE(0);
if (atomSize < 8) return null;
const atomType = headerBuffer.slice(4, 8).toString();
if (atomType === 'moov') {
// Read the entire moov atom, which contains duration info
const moovBuffer = await readFileChunk(filePath, position, atomSize);
// Search for mvhd atom inside moov
for (let offset = 8; offset < moovBuffer.length - 8; offset++) {
if (
offset + 4 <= moovBuffer.length &&
moovBuffer.slice(offset, offset + 4).toString() === 'mvhd'
) {
if (offset + 5 >= moovBuffer.length) break;
const version = moovBuffer[offset + 4];
let timescale, duration;
if (version === 0) {
if (offset + 24 >= moovBuffer.length) break;
timescale = moovBuffer.readUInt32BE(offset + 16);
duration = moovBuffer.readUInt32BE(offset + 20);
} else {
if (offset + 36 >= moovBuffer.length) break;
timescale = moovBuffer.readUInt32BE(offset + 24);
const durationHigh = moovBuffer.readUInt32BE(offset + 28);
const durationLow = moovBuffer.readUInt32BE(offset + 32);
duration = durationHigh * Math.pow(2, 32) + durationLow;
}
if (timescale > 0) {
const durationSec = duration / timescale;
const durationMs = Math.round(durationSec * 1000);
return durationMs.toString();
}
}
}
} else if (['trak', 'mdia', 'minf', 'stbl'].includes(atomType)) {
// Read and recursively search these container atoms
return findMp4Duration(fd, filePath, position + 8);
}
// If we didn't find duration in this atom, try the next one
if (atomSize > 0) {
return findMp4Duration(fd, filePath, position + atomSize);
}
} catch (e) {
return null;
}
return null;
}
export default getAudioFileDuration;