Topic was automatically imported from the old Question2Answer platform.
Asked By
Zylann
I’m trying to read and write samples from an AudioStreamSample as -1…1 float values. But I’m hitting a number of walls while trying to do this.
The doc only says data expects signed PCM8 data. But what about 16 bits? Some other properties of AudioStreamSample are documented as being in bytes, which doesnt seem to make any sense in 16-bits. And yet I even have issues with 8-bits.
So I rolled my own test project to figure this out:
I made a simple sine wave sound with Audacity, with an amplitude not exceeding 0.1 (in the -1…1 linear range).
I imported that sound both as 8bit and 16bits, and tried to read samples using these functions (not returning anything, just checking if I read correctly):
func load_8bits():
var stream = load("res://sample_8bits.wav")
assert(stream.format == AudioStreamSample.FORMAT_8_BITS)
var bytes = stream.data
var i = 0
while i < len(bytes):
var b = bytes[i]
# Despite what the doc says, PoolByteArray contains uint8_t values,
# which are unsigned bytes. So in GDScript we get unsigned integers.
# Subtracting 128 to get back to signed PCM8, I guess
var s = float(b - 128) / 128.0
# Quick amplitude test to verify we got the sample properly
assert(s < 0.2)
i += 1
func load_16bits():
var stream = load("res://sample_16bits.wav")
assert(stream.format == AudioStreamSample.FORMAT_16_BITS)
var bytes = stream.data
var i = 0
# Read by packs of 2 bytes
while i < len(bytes):
var b0 = bytes[i]
var b1 = bytes[i + 1]
# Combine low bits and high bits to obtain 16-bit value (unsigned? signed?)
var u = b0 | (b1 << 8)
# Same here, convert from unsigned to signed
var s = float(u - 32768) / 32768.0
# Quick test to verify we got the sample properly
assert(s < 0.2)
i += 2
But that doesn’t work. The assert fails all the time, which means the obtained samples are invalid (they should not exceed 0.1).
In particular, I checked the 8bit one, it failed because it read a byte which value was 254. If we subtract 128, that’s a value of 126 in PCM8. Which is far too high Oo
Anyone ever managed to read such data properly? I’m pretty sure I did things correctly.
After more fiddling, I realized that converting between unsigned and signed is not straightforward. It requires to emulate 8-bit and 16-bit wrapping.
I updated the functions to be more usable, for future reference:
static func read_8bit_samples(stream: AudioStreamSample) -> Array:
assert(stream.format == AudioStreamSample.FORMAT_8_BITS)
var bytes = stream.data
var samples = []
for i in len(bytes):
var b = bytes[i]
# Despite what the doc says, PoolByteArray contains uint8_t values,
# which are unsigned bytes representing signed numbers.
# In GDScript, we still get positive integers, i.e -2 => 253.
# So we bring back their representation as unsigned,
# emulating the 8-bit wrapping behavior.
var u = (b + 128) & 0xff
# Then bring back to signed -1..1 range
var s = float(u - 128) / 128.0
samples.append(s)
return samples
static func read_16bit_samples(stream: AudioStreamSample) -> Array:
assert(stream.format == AudioStreamSample.FORMAT_16_BITS)
var bytes = stream.data
var samples = []
var i = 0
# Read by packs of 2 bytes
while i < len(bytes):
var b0 = bytes[i]
var b1 = bytes[i + 1]
# Combine low bits and high bits to obtain 16-bit value
var u = b0 | (b1 << 8)
# Emulate signed to unsigned 16-bit conversion
u = (u + 32768) & 0xffff
# Convert to -1..1 range
var s = float(u - 32768) / 32768.0
samples.append(s)
i += 2
return samples
static func write_8bit_samples(samples: Array) -> AudioStreamSample:
var bytes = PoolByteArray()
bytes.resize(len(samples))
for i in len(samples):
var u = int(samples[i] * 128.0) + 128
# Godot will internally cast to byte so we don't need to emulate it.
# This is the only part the doc explains accurately.
bytes[i] = u - 128
var stream = AudioStreamSample.new()
stream.stereo = false
stream.format = AudioStreamSample.FORMAT_8_BITS
stream.data = bytes
return stream
static func write_16bit_samples(samples: Array) -> AudioStreamSample:
var bytes = PoolByteArray()
bytes.resize(len(samples) * 2)
for i in len(samples):
var j = i * 2
var u = int(samples[i] * 32768.0) + 32768
# Emulate cast from unsigned to signed
u = (u - 32768) & 0xffff
# Assign low and high byte
bytes[j] = u & 0xff
bytes[j + 1] = u >> 8
var stream = AudioStreamSample.new()
stream.stereo = false
stream.format = AudioStreamSample.FORMAT_16_BITS
stream.data = bytes
return stream
Note: there is something strange remaining, still.
When I save a 16-bit sound back to disk as wav, the file plays correctly, but is slightly pitched down (about 5%). It may be due to the fact the original sample I used was 48000 Hz, while the default is 44100.