PCM16 distortion
chdh opened this issue · 12 comments
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
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)
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.
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.
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 |
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 |
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.
Thanks for your work.