@@ -9,6 +9,20 @@ import { applyFinalFilters } from '../lib/dsp/final-filters.js';
99// WAV Encoding
1010// ============================================================================
1111
12+ const DITHER_BUFFER_SIZE = 256 ;
13+ const ditherBuffer = new Float32Array ( DITHER_BUFFER_SIZE ) ;
14+ for ( let i = 0 ; i < DITHER_BUFFER_SIZE ; i ++ ) {
15+ ditherBuffer [ i ] = ( Math . random ( ) + Math . random ( ) ) - 1 ;
16+ }
17+ let ditherIndex = 0 ;
18+
19+ function triangularDither ( ) {
20+ // TPDF in integer (LSB) domain: [-1, 1]
21+ const dither = ditherBuffer [ ditherIndex ] ;
22+ ditherIndex = ( ditherIndex + 1 ) % DITHER_BUFFER_SIZE ;
23+ return dither ;
24+ }
25+
1226/**
1327 * Encode an AudioBuffer to WAV format (supports 16-bit and 24-bit)
1428 * @param {AudioBuffer } audioBuffer - Source audio buffer
@@ -18,8 +32,9 @@ import { applyFinalFilters } from '../lib/dsp/final-filters.js';
1832 */
1933export function encodeWAV ( audioBuffer , targetSampleRate , bitDepth ) {
2034 const numChannels = audioBuffer . numberOfChannels ;
35+ const safeBitDepth = bitDepth === 24 ? 24 : 16 ;
2136 const sampleRate = targetSampleRate || audioBuffer . sampleRate ;
22- const bytesPerSample = bitDepth / 8 ;
37+ const bytesPerSample = safeBitDepth / 8 ;
2338
2439 const channelData = [ ] ;
2540 for ( let ch = 0 ; ch < numChannels ; ch ++ ) {
@@ -47,22 +62,27 @@ export function encodeWAV(audioBuffer, targetSampleRate, bitDepth) {
4762 view . setUint32 ( 24 , sampleRate , true ) ;
4863 view . setUint32 ( 28 , sampleRate * numChannels * bytesPerSample , true ) ;
4964 view . setUint16 ( 32 , numChannels * bytesPerSample , true ) ;
50- view . setUint16 ( 34 , bitDepth , true ) ;
65+ view . setUint16 ( 34 , safeBitDepth , true ) ;
5166 writeString ( 36 , 'data' ) ;
5267 view . setUint32 ( 40 , dataSize , true ) ;
5368
5469 let offset = 44 ;
55- const maxVal = bitDepth === 16 ? 32767 : 8388607 ;
70+ const maxVal = safeBitDepth === 16 ? 32767 : 8388607 ;
5671
5772 for ( let i = 0 ; i < numSamples ; i ++ ) {
5873 for ( let ch = 0 ; ch < numChannels ; ch ++ ) {
5974 const sample = Math . max ( - 1 , Math . min ( 1 , channelData [ ch ] [ i ] ) ) ;
60- const intSample = Math . round ( sample * maxVal ) ;
61-
62- if ( bitDepth === 16 ) {
63- view . setInt16 ( offset , intSample , true ) ;
75+ const scaled = sample * maxVal ;
76+ // Dither is only needed for 16-bit export where quantization noise is audible.
77+ const intSample = safeBitDepth === 16
78+ ? Math . round ( scaled + triangularDither ( ) )
79+ : Math . round ( scaled ) ;
80+
81+ if ( safeBitDepth === 16 ) {
82+ const clampedSample = Math . max ( - 32768 , Math . min ( 32767 , intSample ) ) ;
83+ view . setInt16 ( offset , clampedSample , true ) ;
6484 offset += 2 ;
65- } else if ( bitDepth === 24 ) {
85+ } else if ( safeBitDepth === 24 ) {
6686 // Clamp to prevent overflow in bitwise operations
6787 const clampedSample = Math . max ( - 8388607 , Math . min ( 8388607 , intSample ) ) ;
6888 view . setUint8 ( offset , clampedSample & 0xFF ) ;
@@ -145,10 +165,15 @@ export async function encodeWAVAsync(audioBuffer, targetSampleRate, bitDepth, op
145165 for ( let s = i ; s < end ; s ++ ) {
146166 for ( let ch = 0 ; ch < numChannels ; ch ++ ) {
147167 const sample = Math . max ( - 1 , Math . min ( 1 , channelData [ ch ] [ s ] ) ) ;
148- const intSample = Math . round ( sample * maxVal ) ;
168+ const scaled = sample * maxVal ;
169+ // Dither is only needed for 16-bit export where quantization noise is audible.
170+ const intSample = safeBitDepth === 16
171+ ? Math . round ( scaled + triangularDither ( ) )
172+ : Math . round ( scaled ) ;
149173
150174 if ( safeBitDepth === 16 ) {
151- view . setInt16 ( offset , intSample , true ) ;
175+ const clampedSample = Math . max ( - 32768 , Math . min ( 32767 , intSample ) ) ;
176+ view . setInt16 ( offset , clampedSample , true ) ;
152177 offset += 2 ;
153178 } else {
154179 const clampedSample = Math . max ( - 8388607 , Math . min ( 8388607 , intSample ) ) ;
0 commit comments