godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.99k stars 21.16k forks source link

AudioStreamWAV locked to 8bit despite having a 16bit option #83912

Open PeterMarques opened 1 year ago

PeterMarques commented 1 year ago

Godot version

4.1

System information

linux

Issue description

Yes, the data() Method indeed alerts you that it wants a signed 8 bit array (-128 to 127), but there is a format property that has the enum:

● FORMAT_8_BITS = 0 8-bit audio codec. ● FORMAT_16_BITS = 1 16-bit audio codec. ● FORMAT_IMA_ADPCM = 2 Audio is compressed using IMA ADPCM.

So, it should have suport for 16 bit audio in the data.

But, the alert indeed shows to be true, and even if setting the audio to be 16 bits, a random generator from -64 to 64, produces the same dinamic range, in either 8 or 16 bits.

And the lenght of the 16 bit file is half the lenght of the 8 bit one. Thats odd.

image

So, the bitchange instead of changing the depth of volume, changes the lenght of execution.

High resolution audio is a need, and the lenght issue is a undefined behaviours, so that is pretty bugged.

Thats it.

Steps to reproduce

Run the project and see the files (ajust the path)

Minimal reproduction project


extends Node
func _ready():
    var rec1 = AudioStreamWAV.new()
    var rec2 = AudioStreamWAV.new()
    var rec3 = AudioStreamWAV.new()
    var rec4 = AudioStreamWAV.new() 

    var arr1 = PackedByteArray([])
    var arr2 = PackedByteArray([])

    rec1.format = AudioStreamWAV.FORMAT_8_BITS
    rec2.format = AudioStreamWAV.FORMAT_16_BITS
    rec3.format = AudioStreamWAV.FORMAT_8_BITS
    rec4.format = AudioStreamWAV.FORMAT_16_BITS

    for each in 44100 *10:
        arr1.append(randi_range(pow(2,6)*-1,pow(2,6)))

    for each in 44100 *10:
        arr2.append(randi_range(pow(2,14)*-1,pow(2,14)))

    rec1.set_data(arr1)
    rec1.save_to_wav("/home/*USER*/Downloads/Godot4/rec1")

    rec2.set_data(arr1)
    rec2.save_to_wav("/home/*USER*/Downloads/Godot4/rec2")

    rec3.set_data(arr2)
    rec3.save_to_wav("/home/*USER*/Downloads/Godot4/rec3")

    rec4.set_data(arr2)
    rec4.save_to_wav("/home/*USER*/Downloads/Godot4/rec4")
akien-mga commented 1 year ago

PackedByteArray is an array of bytes (8-bit). I assume for things to work with the 16-bit mode, you would need to manually decompose your 16-bit numbers into two bytes (two PackedByteArray elements).

Edit: Checked the code and confirmed:

    int byte_pr_sample = 0;
    switch (format) {
        case AudioStreamWAV::FORMAT_8_BITS:
            byte_pr_sample = 1;
            break;
        case AudioStreamWAV::FORMAT_16_BITS:
            byte_pr_sample = 2;
            break;
        case AudioStreamWAV::FORMAT_IMA_ADPCM:
            byte_pr_sample = 4;
            break;
    }

If so this would need better documentation.

akien-mga commented 1 year ago

I'm not expert on audio or byte array stuff, but I believe you'd have to do something like this for your 16-bit samples:

var size = 44100 * 10
arr2.resize(size * 2)
for each in size:
    var sample = randi_range(-pow(2, 14), pow(2, 14))
    arr2.encode_s16(each * 2, sample)

Edit: It may need to be encode_u16, not sure what's the deal with signedness here. It seems to read the 8-bit stuff as signed and make it unsigned, but read the 16-bit stuff as unsigned directly?

    // Add data
    Vector<uint8_t> stream_data = get_data();
    const uint8_t *read_data = stream_data.ptr();
    switch (format) {
        case AudioStreamWAV::FORMAT_8_BITS:
            for (unsigned int i = 0; i < data_bytes; i++) {
                uint8_t data_point = (read_data[i] + 128);
                file->store_8(data_point);
            }
            break;
        case AudioStreamWAV::FORMAT_16_BITS:
            for (unsigned int i = 0; i < data_bytes / 2; i++) {
                uint16_t data_point = decode_uint16(&read_data[i * 2]);
                file->store_16(data_point);
            }
            break;
        case AudioStreamWAV::FORMAT_IMA_ADPCM:
            //Unimplemented
            break;
    }
PeterMarques commented 1 year ago

I'm not expert on audio or byte array stuff, but I believe you'd have to do something like this for your 16-bit samples:

var size = 44100 * 10
arr2.resize(size * 2)
for each in size:
  var sample = randi_range(-pow(2, 14), pow(2, 14))
  arr2.encode_s16(each * 2, sample)

Edit: It may need to be encode_u16

It is indeed the solution to push the 16 bits long frames via encode_u16 as your script shows.

The code


extends Node
func _ready():
    var size = 44100 *10
    var rec1 = AudioStreamWAV.new()
    var rec2 = AudioStreamWAV.new()
    var rec3 = AudioStreamWAV.new()
    var rec4 = AudioStreamWAV.new() 

    var arr1 = PackedByteArray([])
    var arr2 = PackedByteArray([])

    rec1.format = AudioStreamWAV.FORMAT_8_BITS
    rec2.format = AudioStreamWAV.FORMAT_16_BITS
    rec3.format = AudioStreamWAV.FORMAT_8_BITS
    rec4.format = AudioStreamWAV.FORMAT_16_BITS

    for each in size:
        arr1.append(randi_range(pow(2,6)*-1,pow(2,6)))

    arr2.resize(size * 2)
    for each in size:
        arr2.encode_u16(each*2,randi_range(pow(2,14)*-1,pow(2,14)))

    rec1.set_data(arr1)
    rec1.save_to_wav("/home/peterpm/Downloads/Godot4/rec1")

    rec2.set_data(arr1)
    rec2.save_to_wav("/home/peterpm/Downloads/Godot4/rec2")

    rec3.set_data(arr2)
    rec3.save_to_wav("/home/peterpm/Downloads/Godot4/rec3")

    rec4.set_data(arr2)
    rec4.save_to_wav("/home/peterpm/Downloads/Godot4/rec4")

now produces the files: image

And the rec4 file is the 16bit file with the 16 bit sound, as it need to be.

Should i close the issue or it need to stay opens to fix the docs?

PeterMarques commented 1 year ago

And if working with stereo audio, the channel works in a pair. So, you need to create the 2 buffers and intercalate then so it works correctly.

extends Node
func _ready():
    var size = 44100 *10
    var rec4 = AudioStreamWAV.new() 

    var arr2 = PackedByteArray([])
    var arr3 = PackedByteArray([])
    var arr4 = PackedByteArray([])

    rec4.format = AudioStreamWAV.FORMAT_16_BITS

    rec4.stereo = true

    arr2.resize(size * 2)
    arr3.resize(size * 2)
    for each in size:
        arr2.encode_u16(each*2,randi_range(pow(2,14)*-1,pow(2,14)))
    for each in size:
        arr3.encode_u16(each*2,randi_range(pow(2,12)*-1,pow(2,12)))

    for each in size:
        arr4.append(arr2[each*2])
        arr4.append(arr2[(each*2)+1])

        arr4.append(arr3[each*2])
        arr4.append(arr3[(each*2)+1])

    rec4.set_data(arr4)
    rec4.save_to_wav("/home/peterpm/Downloads/Godot4/rec4")

generates the expected: image

So it need to have that in the docs as well.

All that is counterproductive, but its the nature of the object, but lacks the ease of usage of Vector2 in the AudioStreamGenerator in the pushframe(), that handles both the bitdepth and the stereo issue in a single pass

well, just need better documentation in the AudioStreamWAV object.

And, there is another more performant way to mescle the arrays?