UNPKG

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
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