Menu

#294 Wrong conversion from 8 bit to 16 bit

closed-rejected
nobody
None
5
2025-08-13
2017-07-20
No
  1. Create a raw file (test.raw):
0x55 0x53 0x20 0x24
  1. Convert it using this command:
sox --encoding unsigned-integer --bits 8 --endian big --channels 1 --rate 22050 test.raw --encoding unsigned-integer --bits 16 --endian big --channels 1 --rate 22050 test16.raw
  1. You will get these samples:
 0x5500 0x5300 0x2000 0x2400

But it is wrong. You're using the wrong formula.

Look. For example, we have a sample 0xFF:

0xFF=255
255/256=0.99609375
0.99609375*65536=65280
65280=0xFF00

Initial value had maximum possible amplitude, but after conversion we have a bit lower volume of the sound.

The right calculations are:

0xFF=255
255/255=1.0
1.0*65535=65535
65535=0xFFFF

It will work for every value. Generally, we have to do x << 8 + x for conversion from 8 to 16 bit. Why 255 and 65535 instead of 256 and 65536? Just imagine a line between 0 and 255 (maximum possible value for 8 bit integer). You will have 256 dots on this line (number of possible values), but only 255 of them will have a segment on this line.

For example:
0_1_2_3_4_5
There are 6 numbers, but only five segments (underscores).

Discussion

  • Mans Rullgard

    Mans Rullgard - 2020-08-03
    • status: open --> closed-rejected
     
  • Mans Rullgard

    Mans Rullgard - 2020-08-03

    The existing method is reversible using the normal narrowing conversion. This is more important than utilising the entire 16-bit range.

     
    • Evgeny Vrublevsky

      But the proposed method is also reversible by using usual narrowing conversion. And it produces much more logical result. Imagine conversion to FLOAT32. When you convert uint8 255 to float, you get 1.0. When you convert uint16 65535 to float, you get 1.0. But when you convert uint8 255 to uint16 uisng your method, and then convert it to float, you will get wrong value. It is unexpected.

       

      Last edit: Evgeny Vrublevsky 2020-08-03
      • Mans Rullgard

        Mans Rullgard - 2020-08-03

        Converting back after using your method gives different results for negative values.

        There is no loss of information, so I don't see what the problem is.

         
        • Evgeny Vrublevsky

          But there is loss of information. uint8 -> float and uint8 -> uint16 -> float will give you different results, but it shouldn't. The maximum amplitude will be a bit smaller.

          Actually, I created this ticket exactly because I proposed to my friend to use SoX to convert stream of 8-bit samples to 16-bit, and then he complained me that it affected the sound somehow. It was confusing, but later we understood what was the reason. Conversion from 8-bit to 16-bit shouldn't cause any difference to the signal. Now, when you convert uint8 to uint16, and when you play the original file, and the new file, you will have a bit different results.

          The same thing can be implemented for signed samples also. The simplest approach is to make int8 -> uint8 -> uint16 -> int16 implicitly, if you want to use the (x << 8) + x formula. Conversion from signed to unsigned and back is lossless.

           

          Last edit: Evgeny Vrublevsky 2020-08-03
          • Mans Rullgard

            Mans Rullgard - 2020-08-03

            The process is reversible, so there can be no loss of information.

            You're losing about 0.03 dB of volume (256 / 257). Nobody can hear that.

             
            • Evgeny Vrublevsky

              There is loss of information. Yeah, if you convert uint8 -> uint16 -> uint8 using current buggy method, you will get the same result. But it will happen just because conversion from uint16 to uint8 is lossy, and the new precision is not enough to represent the difference which was caused by current buggy conversion from uint8 to uint16.

              float32 has enough precision to represent the difference. When you use current method, uint8 -> uint16 -> float32 will have measurable difference from uint8 -> float32.

              When I reported it, I considered it as a bug which is not so hard to fix. It would make the tool a bit better than it is. Don't know why you object against this improvement so much. I do agree, that the difference is small, but it is there. It is always better to be more precise and less lax if there are no other drawbacks.

               

              Last edit: Evgeny Vrublevsky 2020-08-03
              • Mans Rullgard

                Mans Rullgard - 2020-08-04

                You consider it a bug. I don't. You say it's easy to "fix." It isn't. I am the maintainer. You are not. End of story.

                 
                • Evgeny Vrublevsky

                  As far as I can see in the code, you just need to fix a few macros:

                  SOX_UNSIGNED_8BIT_TO_SAMPLE
                  SOX_SIGNED_8BIT_TO_SAMPLE
                  SOX_UNSIGNED_16BIT_TO_SAMPLE
                  SOX_SIGNED_16BIT_TO_SAMPLE
                  SOX_UNSIGNED_24BIT_TO_SAMPLE
                  SOX_SIGNED_24BIT_TO_SAMPLE
                  

                  It seems quite easy to fix, I don't see any issues with it. The fact that the sox_sample_t is signed is not an issue at all, it requires just an additional bit operation and that's it. If I make a patch, will you accept it?

                   

                  Last edit: Evgeny Vrublevsky 2020-08-04
  • Martin Guy

    Martin Guy - 2025-08-13

    I am interested in this as I've had a suspicion that it's rounding towards zero instead of in the same direction. https://codeberg.org/sox_ng/sox_ng/issues/564

     

Log in to post a comment.

MongoDB Logo MongoDB