diff --git a/lib/audio/export.ts b/lib/audio/export.ts index 8c57a97..4ef7f37 100644 --- a/lib/audio/export.ts +++ b/lib/audio/export.ts @@ -136,8 +136,12 @@ export async function audioBufferToMp3( audioBuffer: AudioBuffer, options: ExportOptions = { format: 'mp3', bitrate: 192 } ): Promise { - // Dynamically import lamejs from GitHub repo (has proper browser build) - const lamejs = await import('lamejs'); + // Import lamejs modules directly + const [{ default: MPEGMode }, { default: Lame }, { default: BitStream }] = await Promise.all([ + import('lamejs/src/js/MPEGMode'), + import('lamejs/src/js/Lame'), + import('lamejs/src/js/BitStream'), + ]); const { bitrate = 192, normalize } = options; const numberOfChannels = Math.min(audioBuffer.numberOfChannels, 2); // MP3 supports max 2 channels @@ -167,26 +171,51 @@ export async function audioBufferToMp3( rightPcm[i] = Math.max(-32768, Math.min(32767, (right[i] / peak) * 32767)); } - // Encode - const mp3encoder = new lamejs.Mp3Encoder(numberOfChannels, sampleRate, bitrate); + // Create encoder using lamejs modules + const lame = new Lame(); + const gfp = lame.lame_init(); + + gfp.num_channels = numberOfChannels; + gfp.in_samplerate = sampleRate; + gfp.brate = bitrate; + gfp.mode = numberOfChannels === 1 ? MPEGMode.MONO : MPEGMode.STEREO; + gfp.quality = 3; // 0=best (very slow), 9=worst (fast) + gfp.write_id3tag_automatic = 0; + + lame.lame_init_params(gfp); + const mp3Data: Int8Array[] = []; + const mp3buf = new Int8Array(samples * 1.25 + 7200); // Estimate output size const sampleBlockSize = 1152; // Standard MP3 frame size for (let i = 0; i < samples; i += sampleBlockSize) { - const leftChunk = leftPcm.subarray(i, i + sampleBlockSize); - const rightChunk = numberOfChannels > 1 ? rightPcm.subarray(i, i + sampleBlockSize) : leftChunk; - const mp3buf = mp3encoder.encodeBuffer(leftChunk, rightChunk); - if (mp3buf.length > 0) { - mp3Data.push(mp3buf); + const leftChunk = leftPcm.subarray(i, Math.min(i + sampleBlockSize, samples)); + const rightChunk = numberOfChannels > 1 + ? rightPcm.subarray(i, Math.min(i + sampleBlockSize, samples)) + : leftChunk; + + const bytesEncoded = lame.lame_encode_buffer( + gfp, + leftChunk, + rightChunk, + leftChunk.length, + mp3buf, + 0 + ); + + if (bytesEncoded > 0) { + mp3Data.push(new Int8Array(mp3buf.subarray(0, bytesEncoded))); } } // Flush remaining data - const mp3buf = mp3encoder.flush(); - if (mp3buf.length > 0) { - mp3Data.push(mp3buf); + const flushBytes = lame.lame_encode_flush(gfp, mp3buf, 0); + if (flushBytes > 0) { + mp3Data.push(new Int8Array(mp3buf.subarray(0, flushBytes))); } + lame.lame_close(gfp); + // Combine all chunks const totalLength = mp3Data.reduce((acc, arr) => acc + arr.length, 0); const result = new Uint8Array(totalLength); diff --git a/types/lamejs.d.ts b/types/lamejs.d.ts index 4dac477..7035746 100644 --- a/types/lamejs.d.ts +++ b/types/lamejs.d.ts @@ -1,7 +1,31 @@ -declare module 'lamejs' { - export class Mp3Encoder { - constructor(channels: number, sampleRate: number, bitrate: number); - encodeBuffer(left: Int16Array, right: Int16Array): Int8Array; - flush(): Int8Array; +declare module 'lamejs/src/js/MPEGMode' { + const MPEGMode: { + STEREO: number; + JOINT_STEREO: number; + DUAL_CHANNEL: number; + MONO: number; + NOT_SET: number; + }; + export default MPEGMode; +} + +declare module 'lamejs/src/js/Lame' { + export default class Lame { + lame_init(): any; + lame_init_params(gfp: any): number; + lame_encode_buffer( + gfp: any, + left: Int16Array, + right: Int16Array, + samples: number, + mp3buf: Int8Array, + mp3bufPos: number + ): number; + lame_encode_flush(gfp: any, mp3buf: Int8Array, mp3bufPos: number): number; + lame_close(gfp: any): number; } } + +declare module 'lamejs/src/js/BitStream' { + export default class BitStream {} +}