@@ -6,12 +6,23 @@ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
66import { dirname , join } from "node:path" ;
77import { tmpdir } from "node:os" ;
88
9- import type { LameOptionsBag , LameProgressEmitter , LameStatus } from "../types" ;
9+ import type {
10+ BitWidth ,
11+ LameOptionsBag ,
12+ LameProgressEmitter ,
13+ LameStatus ,
14+ } from "../types" ;
1015import { resolveLameBinary } from "../internal/binary/resolve-binary" ;
1116import { LameOptions } from "./lame-options" ;
1217
1318type ProgressKind = "encode" | "decode" ;
1419
20+ function isFloatArray (
21+ view : ArrayBufferView ,
22+ ) : view is Float32Array | Float64Array {
23+ return view instanceof Float32Array || view instanceof Float64Array ;
24+ }
25+
1526function parseEncodeProgressLine ( content : string ) : {
1627 progress ?: number ;
1728 eta ?: string ;
@@ -105,12 +116,12 @@ class Lame {
105116 return this ;
106117 }
107118
108- public setBuffer ( file : Buffer ) : this {
109- if ( ! Buffer . isBuffer ( file ) ) {
110- throw new Error ( "Audio file (buffer) does not exist" ) ;
111- }
119+ public setBuffer (
120+ file : Buffer | ArrayBuffer | ArrayBufferView ,
121+ ) : this {
122+ const normalized = this . normalizeInputBuffer ( file ) ;
112123
113- this . fileBuffer = file ;
124+ this . fileBuffer = normalized ;
114125 this . filePath = undefined ;
115126
116127 return this ;
@@ -362,16 +373,238 @@ class Lame {
362373 } ) ;
363374 }
364375
376+ private normalizeInputBuffer (
377+ input : Buffer | ArrayBuffer | ArrayBufferView ,
378+ ) : Buffer {
379+ if ( Buffer . isBuffer ( input ) ) {
380+ return input ;
381+ }
382+
383+ if ( input instanceof ArrayBuffer ) {
384+ return Buffer . from ( new Uint8Array ( input ) ) ;
385+ }
386+
387+ if ( ArrayBuffer . isView ( input ) ) {
388+ return this . convertArrayViewToBuffer ( input ) ;
389+ }
390+
391+ throw new Error ( "Audio file (buffer) does not exist" ) ;
392+ }
393+
394+ private convertArrayViewToBuffer ( view : ArrayBufferView ) : Buffer {
395+ if ( isFloatArray ( view ) ) {
396+ return this . convertFloatArrayToBuffer ( view ) ;
397+ }
398+
399+ const uintView = new Uint8Array (
400+ view . buffer ,
401+ view . byteOffset ,
402+ view . byteLength ,
403+ ) ;
404+
405+ return Buffer . from ( uintView ) ;
406+ }
407+
408+ private convertFloatArrayToBuffer (
409+ view : Float32Array | Float64Array ,
410+ ) : Buffer {
411+ const bitwidth : BitWidth = this . options . bitwidth ?? 16 ;
412+ const useBigEndian = this . shouldUseBigEndian ( ) ;
413+ const isSigned = this . isSignedForBitwidth ( bitwidth ) ;
414+
415+ switch ( bitwidth ) {
416+ case 8 : {
417+ const buffer = Buffer . alloc ( view . length ) ;
418+
419+ for ( let index = 0 ; index < view . length ; index += 1 ) {
420+ const sample = view [ index ] ;
421+
422+ if ( isSigned ) {
423+ const value = this . scaleToSignedInteger ( sample , 8 ) ;
424+ buffer . writeInt8 ( value , index ) ;
425+ } else {
426+ const value = this . scaleToUnsignedInteger ( sample , 8 ) ;
427+ buffer . writeUInt8 ( value , index ) ;
428+ }
429+ }
430+
431+ return buffer ;
432+ }
433+
434+ case 16 : {
435+ if ( ! isSigned ) {
436+ throw new Error (
437+ "lame: Float PCM input only supports signed samples for bitwidth 16" ,
438+ ) ;
439+ }
440+
441+ const buffer = Buffer . alloc ( view . length * 2 ) ;
442+ for ( let index = 0 ; index < view . length ; index += 1 ) {
443+ const value = this . scaleToSignedInteger ( view [ index ] , 16 ) ;
444+ const offset = index * 2 ;
445+
446+ if ( useBigEndian ) {
447+ buffer . writeInt16BE ( value , offset ) ;
448+ } else {
449+ buffer . writeInt16LE ( value , offset ) ;
450+ }
451+ }
452+
453+ return buffer ;
454+ }
455+
456+ case 24 : {
457+ if ( ! isSigned ) {
458+ throw new Error (
459+ "lame: Float PCM input only supports signed samples for bitwidth 24" ,
460+ ) ;
461+ }
462+
463+ const buffer = Buffer . alloc ( view . length * 3 ) ;
464+ for ( let index = 0 ; index < view . length ; index += 1 ) {
465+ const offset = index * 3 ;
466+ const scaled = this . scaleToSignedInteger ( view [ index ] , 24 ) ;
467+ let value = scaled ;
468+
469+ if ( value < 0 ) {
470+ value += 1 << 24 ;
471+ }
472+
473+ if ( useBigEndian ) {
474+ buffer [ offset ] = ( value >> 16 ) & 0xff ;
475+ buffer [ offset + 1 ] = ( value >> 8 ) & 0xff ;
476+ buffer [ offset + 2 ] = value & 0xff ;
477+ } else {
478+ buffer [ offset ] = value & 0xff ;
479+ buffer [ offset + 1 ] = ( value >> 8 ) & 0xff ;
480+ buffer [ offset + 2 ] = ( value >> 16 ) & 0xff ;
481+ }
482+ }
483+
484+ return buffer ;
485+ }
486+
487+ case 32 : {
488+ if ( ! isSigned ) {
489+ throw new Error (
490+ "lame: Float PCM input only supports signed samples for bitwidth 32" ,
491+ ) ;
492+ }
493+
494+ const buffer = Buffer . alloc ( view . length * 4 ) ;
495+ for ( let index = 0 ; index < view . length ; index += 1 ) {
496+ const value = this . scaleToSignedInteger ( view [ index ] , 32 ) ;
497+ const offset = index * 4 ;
498+
499+ if ( useBigEndian ) {
500+ buffer . writeInt32BE ( value , offset ) ;
501+ } else {
502+ buffer . writeInt32LE ( value , offset ) ;
503+ }
504+ }
505+
506+ return buffer ;
507+ }
508+ }
509+ }
510+
511+ private shouldUseBigEndian ( ) : boolean {
512+ if ( this . options [ "big-endian" ] === true ) {
513+ return true ;
514+ }
515+
516+ if ( this . options [ "little-endian" ] === true ) {
517+ return false ;
518+ }
519+
520+ return false ;
521+ }
522+
523+ private isSignedForBitwidth ( bitwidth : BitWidth ) : boolean {
524+ if ( bitwidth === 8 ) {
525+ if ( this . options . unsigned === true ) {
526+ return false ;
527+ }
528+
529+ return this . options . signed === true ;
530+ }
531+
532+ if ( this . options . unsigned === true ) {
533+ return false ;
534+ }
535+
536+ return true ;
537+ }
538+
539+ private clampSample ( value : number ) : number {
540+ if ( ! Number . isFinite ( value ) ) {
541+ return 0 ;
542+ }
543+
544+ if ( value <= - 1 ) {
545+ return - 1 ;
546+ }
547+
548+ if ( value >= 1 ) {
549+ return 1 ;
550+ }
551+
552+ return value ;
553+ }
554+
555+ private scaleToSignedInteger ( value : number , bitwidth : BitWidth ) : number {
556+ const clamped = this . clampSample ( value ) ;
557+
558+ const positiveMax = Math . pow ( 2 , bitwidth - 1 ) - 1 ;
559+ const negativeScale = Math . pow ( 2 , bitwidth - 1 ) ;
560+
561+ if ( clamped <= - 1 ) {
562+ return - negativeScale ;
563+ }
564+
565+ if ( clamped >= 1 ) {
566+ return positiveMax ;
567+ }
568+
569+ if ( clamped < 0 ) {
570+ const scaled = Math . round ( clamped * negativeScale ) ;
571+ return Math . max ( - negativeScale , scaled ) ;
572+ }
573+
574+ const scaled = Math . round ( clamped * positiveMax ) ;
575+ return Math . min ( positiveMax , scaled ) ;
576+ }
577+
578+ private scaleToUnsignedInteger ( value : number , bitwidth : BitWidth ) : number {
579+ const clamped = this . clampSample ( value ) ;
580+ const normalized = ( clamped + 1 ) / 2 ;
581+ const max = Math . pow ( 2 , bitwidth ) - 1 ;
582+ const scaled = Math . round ( normalized * max ) ;
583+
584+ return Math . min ( Math . max ( scaled , 0 ) , max ) ;
585+ }
586+
365587 private async persistInputBufferToTempFile (
366588 type : ProgressKind ,
367589 ) : Promise < string > {
368590 const tempPath = await this . generateTempFilePath ( "raw" , type ) ;
369- const inputView = Uint8Array . from ( this . fileBuffer ! ) ;
370- await writeFile ( tempPath , inputView ) ;
591+ await writeFile ( tempPath , this . toUint8Array ( this . fileBuffer ! ) ) ;
371592 this . fileBufferTempFilePath = tempPath ;
372593 return tempPath ;
373594 }
374595
596+ private toUint8Array ( view : Buffer | ArrayBufferView ) : Uint8Array {
597+ if ( view instanceof Buffer ) {
598+ return new Uint8Array (
599+ view . buffer ,
600+ view . byteOffset ,
601+ view . byteLength ,
602+ ) ;
603+ }
604+
605+ return new Uint8Array ( view . buffer , view . byteOffset , view . byteLength ) ;
606+ }
607+
375608 private async prepareOutputTarget ( type : ProgressKind ) : Promise < {
376609 outputPath : string ;
377610 bufferOutput : boolean ;
0 commit comments