bastibe / python-soundfile

SoundFile is an audio library based on libsndfile, CFFI, and NumPy
BSD 3-Clause "New" or "Revised" License
676 stars 105 forks source link

24-bit data not written/read properly #436

Closed GABowers closed 1 day ago

GABowers commented 1 week ago

Considering the following script:

import soundfile as sf, numpy as np, scipy.io.wavfile, os

def get_bits(val, bit:int = 24):
    return [val & 1 << b and 1 for b in range(bit)]

def get_val(bits:list, bit:int = 16):
    if len(bits) < bit:
        raise Exception('Attempting {}-bit conversion on {} bits of data'.format(bit, len(bits)))
    cnt = 0
    for i in range(bit):
        cnt += bits[i] * 2**i
    return cnt

if __name__ == "__main__":
    fn = './test_24bit.wav'
    dat = np.array([2**0, 2**1, 2**2, 2**3, 2**4, 2**5, 2**6, 2**7, 2**8, 2**9])
    print('{}'.format(dat))

    dat2 = np.array([get_val(get_bits(x), 24) for x in dat])
    print('{}'.format(dat2))

    print('24-bit')
    with sf.SoundFile(fn, mode='w', samplerate=2048, channels=1, subtype='PCM_24') as f:
        f.write(dat2)

    fs, dat3 = scipy.io.wavfile.read(fn)
    print('{}'.format(dat3))

    with sf.SoundFile(fn) as f:
        dat4 = f.read(dtype='int32')
        print('{}'.format(dat4))

        dat5 = [get_val(get_bits(x), 24) for x in dat4]
        print('{}'.format(dat5[:10]))
    os.remove(os.path.abspath(fn))

    print('32-bit')
    with sf.SoundFile(fn, mode='w', samplerate=2048, channels=1, subtype='PCM_32') as f:
        f.write(dat2)

    fs, dat3 = scipy.io.wavfile.read(fn)
    print('{}'.format(dat3))

    with sf.SoundFile(fn) as f:
        dat4 = f.read(dtype='int32')
        print('{}'.format(dat4))

        dat5 = [get_val(get_bits(x), 24) for x in dat4]
        print('{}'.format(dat5))
    os.remove(os.path.abspath(fn))

    print('e')

A numpy array of 32-bit ints is created, then passed through a couple of functions to ensure the data is 24-bit. This is then saved with soundfile as a PCM_24 datatype, then read in multiple ways. It is then saved with a PCM_32 datatype then read back in, in multiple ways.

The expected behavior would be for the data to be the same after reading it back in, regardless of how it was saved. However the output on my machine is:

[ 1 2 4 8 16 32 64 128 256 512] [ 1 2 4 8 16 32 64 128 256 512] 24-bit [ 0 0 0 0 0 0 0 0 256 512] [ 0 0 0 0 0 0 0 0 256 512] [0, 0, 0, 0, 0, 0, 0, 0, 256, 512] 32-bit [ 1 2 4 8 16 32 64 128 256 512] [ 1 2 4 8 16 32 64 128 256 512] [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] e

It seems clear it's dropping those first 8 bits when it saves, when it should be dropping the last 8 bits. Is this correct, or is there some other manner of creating 24-bit integers in python that soundfile is expecting and which will be saved properly?

(I've also run it without lines 19-20--saving the 32 bit data directly--and it gives the same result)

bastibe commented 2 days ago

I can not reproduce this.

Writing and reading a 24-bit WAV file reproduces the original data perfectly:

soundfile.write('test.wav', [2**n/2**23 for n in range(10)], samplerate=48000, subtype='PCM_24')
print(soundfile.read('test.wav')[0] * 2**23)  # array([  1.,   2.,   4.,   8.,  16.,  32.,  64., 128., 256., 512.])
GABowers commented 2 days ago

This line [2**n/2**23 for n in range(10)] produces floating point values, not integers. The following has the error:

soundfile.write('test.wav', [2**n for n in range(10)], samplerate=48000, subtype='PCM_24')
print(soundfile.read('test.wav', dtype=np.int32)[0])

Is PCM_24 not meant to work with integers? Is there some other way to store 24 bit integers?

bastibe commented 1 day ago

No, the user data is always float in -1...1. The subtype merely specifies how this data is serialized.

GABowers commented 1 day ago

Good to know - thanks for the information!