mohayonao/wav-encoder

PCM16 distortion

chdh opened this issue · 12 comments

chdh commented

For the PCM16 format, negative sample values are multiplied by 32768, but positive sample values are multiplied by 32767. This leads to a small distortion.

Consider the following symmetrical float signal without a DC value: 0, 0.5, 0, -0.5, 0
The values written to the WAV file are: 0, 16383, 0, -16384, 0

(0.5 * 32767)|0 ==> 16383
(-0.5 * 32768)|0 ==> -16384

chdh commented

I don't understand the fix. This doesn't solve the problem.

Example:
Math.round(0.99 * 32767) ==> 32439
Math.round(-0.99 * 32768) ==> -32440

Why not just use:
value = Math.round(value * 32767) | 0
(without a distinction whether its negative or positive)

chdh commented

I had a look at how Google Chrome and Firefox decode WAV files with AudioContext.decodeAudioData():

WAV hex decimal Chrome Firefox
0x7FFF 32767 1 0.999969482421875
0x7FFE 32766 0.999969482421875 0.99993896484375
0x4000 16384 0.5000152587890625 0.5
0x3FFF 16383 0.4999847412109375 0.499969482421875
0x0000 0 0 0
0xBFFF -16385 -0.500030517578125 -0.500030517578125
0xC000 -16384 -0.5 -0.5
0x8002 -32766 -0.99993896484375 -0.99993896484375
0x8001 -32767 -0.999969482421875 -0.999969482421875
0x8000 -32768 -1 -1

Chrome uses an asymmetric scheme, like you currently do, and can encode the float value 1.
Firefox uses a symmetric scheme, but cannot encode the float value 1.

Why not map the integer range -32768 .. +32767 to the float range -1.0000305185 .. +1? This would be symmetric and encoding of the float value 1 would be possible.

It seems to me that is decoder problem.

chdh commented

I agree, The problem occurs when the encoder and decoder do not use the same mapping between float (-1 to 1) and int (-32768 to 32767). Your encoder and decoder use the same mapping as the decoder of Google Chrome, but Firefox is different.

This issue move to mohayonao/wav-decoder#14.

chdh commented

The same "symmetric" option would also make sense for the encoder.

How do encoder map values with symmetric options with int16?

signal default symmetric
1 32767
0.5 16384
0 0
-0.5 -16384
-1 -32768
chdh commented
signal default symmetric V1 symmetric V2 (Firefox)
1 32767 32767 32767 (clipped)
0.9999694824 32766 32766 32767
0.75 24575 24575 24576
0.5 16384 16384 16384
0 0 0 0
-0.5 -16384 -16384 -16384
-1 -32768 -32767 -32768
-1.0000305185 -32768 (clipped) -32768 -32768 (clipped)

Thanks.

I can't accept "symmetric V1".
Because, "symmetric V1" encode -1 to -32767, but decoder does not decode -32767 to -1.

The code of "symmetric V2" is below? I agree that is not bad as optional variation.

function encode_pcm16(value) {
  value = Math.max(-1, Math.min(value, +1));

  return Math.min(Math.round(value * 32768), 32767);
}
abs signal positive negative
1 32768 -32767
0.9999847412109375 32768 -32767
0.9999846816062927 32767 -32767
chdh commented

or shorter:

function encode_pcm16(value) {
   return Math.max(-32768, Math.min(32767, Math.round(value * 32768)));
}

Okay. I've implemented and merged it to v1.3.0. Thanks.

chdh commented

Thanks for your work.