audio.libx.js
Version:
Comprehensive audio library with progressive streaming, recording capabilities, real-time processing, and intelligent caching for web applications
271 lines • 10.8 kB
JavaScript
import { ProcessingError } from './types.js';
export class AudioProcessor {
constructor() {
this._audioContext = null;
this._initializeAudioContext();
}
_initializeAudioContext() {
try {
if (typeof window !== 'undefined' && 'AudioContext' in window) {
this._audioContext = new AudioContext();
}
else if (typeof window !== 'undefined' && 'webkitAudioContext' in window) {
this._audioContext = new window.webkitAudioContext();
}
}
catch (error) {
console.warn('AudioContext not available:', error);
}
}
async processAudio(chunks, options = {}) {
const { trimSilence = true, silenceThresholdDb = -50, minSilenceMs = 100, outputFormat = 'wav', stripID3 = true } = options;
try {
let processedChunks = stripID3 ? this._stripID3Tags(chunks) : chunks;
const arrayBuffer = this._concatenateChunks(processedChunks);
let finalBuffer = arrayBuffer;
let metadata = {
originalDuration: 0,
trimmedDuration: 0,
silenceRemovedStart: 0,
silenceRemovedEnd: 0
};
if (trimSilence && this._audioContext) {
const audioBuffer = await this._decodeAudioData(arrayBuffer);
metadata.originalDuration = audioBuffer.duration;
const trimmedBuffer = this._trimSilence(audioBuffer, silenceThresholdDb, minSilenceMs);
metadata.trimmedDuration = trimmedBuffer.buffer.duration;
metadata.silenceRemovedStart = trimmedBuffer.trimmedStart;
metadata.silenceRemovedEnd = trimmedBuffer.trimmedEnd;
if (outputFormat === 'wav') {
finalBuffer = this._audioBufferToWav(trimmedBuffer.buffer);
}
else {
finalBuffer = this._audioBufferToWav(trimmedBuffer.buffer);
}
}
else if (outputFormat === 'wav' && this._audioContext) {
const audioBuffer = await this._decodeAudioData(arrayBuffer);
metadata.originalDuration = audioBuffer.duration;
metadata.trimmedDuration = audioBuffer.duration;
finalBuffer = this._audioBufferToWav(audioBuffer);
}
const blob = new Blob([finalBuffer], {
type: outputFormat === 'wav' ? 'audio/wav' : 'audio/mpeg'
});
return {
blob,
metadata
};
}
catch (error) {
throw new ProcessingError('Failed to process audio', undefined, error);
}
}
_trimSilence(audioBuffer, silenceThresholdDb, minSilenceMs) {
if (!this._audioContext) {
throw new ProcessingError('AudioContext not available for trimming');
}
const sampleRate = audioBuffer.sampleRate;
const channelData = audioBuffer.getChannelData(0);
const threshold = Math.pow(10, silenceThresholdDb / 20);
const minSilenceSamples = (minSilenceMs / 1000) * sampleRate;
let startSample = 0;
let endSample = channelData.length - 1;
for (let i = 0; i < channelData.length; i++) {
if (Math.abs(channelData[i]) > threshold) {
startSample = Math.max(0, i - Math.floor(minSilenceSamples / 2));
break;
}
}
for (let i = channelData.length - 1; i >= 0; i--) {
if (Math.abs(channelData[i]) > threshold) {
endSample = Math.min(channelData.length - 1, i + Math.floor(minSilenceSamples / 2));
break;
}
}
const trimmedLength = endSample - startSample + 1;
if (trimmedLength <= 0) {
const minimalBuffer = this._audioContext.createBuffer(audioBuffer.numberOfChannels, Math.floor(sampleRate * 0.1), sampleRate);
return {
buffer: minimalBuffer,
trimmedStart: 0,
trimmedEnd: 0
};
}
const trimmedBuffer = this._audioContext.createBuffer(audioBuffer.numberOfChannels, trimmedLength, sampleRate);
for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
const originalData = audioBuffer.getChannelData(ch);
const trimmedData = trimmedBuffer.getChannelData(ch);
trimmedData.set(originalData.slice(startSample, endSample + 1));
}
return {
buffer: trimmedBuffer,
trimmedStart: startSample / sampleRate,
trimmedEnd: (channelData.length - endSample - 1) / sampleRate
};
}
_stripID3Tags(chunks) {
return chunks.map((chunk, index) => this._stripID3FromChunk(chunk, index === 0));
}
_stripID3FromChunk(chunk, keepID3v2 = false) {
let start = 0;
let end = chunk.length;
if (chunk.length >= 10 &&
chunk[0] === 0x49 && chunk[1] === 0x44 && chunk[2] === 0x33) {
const size = ((chunk[6] & 0x7f) << 21) |
((chunk[7] & 0x7f) << 14) |
((chunk[8] & 0x7f) << 7) |
(chunk[9] & 0x7f);
const tagEnd = 10 + size;
if (!keepID3v2 && tagEnd < chunk.length) {
start = tagEnd;
}
}
if (chunk.length >= 128 &&
chunk[end - 128] === 0x54 &&
chunk[end - 127] === 0x41 &&
chunk[end - 126] === 0x47) {
end -= 128;
}
return chunk.subarray(start, end);
}
_concatenateChunks(chunks) {
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.byteLength;
}
return combined.buffer;
}
async _decodeAudioData(arrayBuffer) {
if (!this._audioContext) {
throw new ProcessingError('AudioContext not available for decoding');
}
try {
if (this._audioContext.state === 'suspended') {
await this._audioContext.resume();
}
return await this._audioContext.decodeAudioData(arrayBuffer.slice(0));
}
catch (error) {
throw new ProcessingError('Failed to decode audio data', undefined, error);
}
}
_audioBufferToWav(buffer) {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const format = 1;
const bitDepth = 16;
const samples = buffer.length;
const blockAlign = numChannels * bitDepth / 8;
const byteRate = sampleRate * blockAlign;
const dataSize = samples * blockAlign;
const bufferLength = 44 + dataSize;
const arrayBuffer = new ArrayBuffer(bufferLength);
const view = new DataView(arrayBuffer);
let offset = 0;
const writeString = (s) => {
for (let i = 0; i < s.length; i++) {
view.setUint8(offset++, s.charCodeAt(i));
}
};
const writeUint32 = (value) => {
view.setUint32(offset, value, true);
offset += 4;
};
const writeUint16 = (value) => {
view.setUint16(offset, value, true);
offset += 2;
};
writeString('RIFF');
writeUint32(36 + dataSize);
writeString('WAVE');
writeString('fmt ');
writeUint32(16);
writeUint16(format);
writeUint16(numChannels);
writeUint32(sampleRate);
writeUint32(byteRate);
writeUint16(blockAlign);
writeUint16(bitDepth);
writeString('data');
writeUint32(dataSize);
for (let i = 0; i < samples; i++) {
for (let ch = 0; ch < numChannels; ch++) {
let sample = buffer.getChannelData(ch)[i];
sample = Math.max(-1, Math.min(1, sample));
const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(offset, intSample, true);
offset += 2;
}
}
return arrayBuffer;
}
concatenateAudioBuffers(buffers) {
if (!this._audioContext || buffers.length === 0) {
return null;
}
const numChannels = buffers[0].numberOfChannels;
const sampleRate = buffers[0].sampleRate;
const totalLength = buffers.reduce((sum, buffer) => sum + buffer.length, 0);
const output = this._audioContext.createBuffer(numChannels, totalLength, sampleRate);
for (let channel = 0; channel < numChannels; channel++) {
let offset = 0;
for (const buffer of buffers) {
output.getChannelData(channel).set(buffer.getChannelData(channel), offset);
offset += buffer.length;
}
}
return output;
}
splitIntoChunks(data, chunkSize = 64 * 1024) {
const chunks = [];
for (let offset = 0; offset < data.length; offset += chunkSize) {
const end = Math.min(offset + chunkSize, data.length);
chunks.push(data.subarray(offset, end));
}
return chunks;
}
validateMP3Chunk(chunk) {
if (chunk.length < 4)
return false;
if (chunk[0] === 0x49 && chunk[1] === 0x44 && chunk[2] === 0x33) {
return true;
}
for (let i = 0; i < chunk.length - 1; i++) {
if (chunk[i] === 0xFF && (chunk[i + 1] & 0xE0) === 0xE0) {
return true;
}
}
return false;
}
estimateDuration(chunks, format) {
const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
switch (format.type) {
case 'mp3':
return (totalBytes * 8) / (128 * 1000);
case 'wav':
return totalBytes / (44100 * 2 * 2);
default:
return totalBytes / (128 * 1000 / 8);
}
}
dispose() {
if (this._audioContext && this._audioContext.state !== 'closed') {
this._audioContext.close();
}
this._audioContext = null;
}
getCapabilities() {
return {
hasAudioContext: this._audioContext !== null,
canTrimSilence: this._audioContext !== null,
canConvertToWav: this._audioContext !== null,
canConcatenate: this._audioContext !== null,
supportedFormats: ['mp3', 'wav', 'ogg', 'webm']
};
}
}
//# sourceMappingURL=AudioProcessor.js.map