Noise and distortion in Swift implementation of SDL audio callback

Also posted on Stack Overflow.

I’m currently building a game music player in Swift for Mac OS, using GME for generating sound buffers and SDL for audio playback. I have previously (and successfully) used SDL_QueueAudio for playback, but I need more fine tuned control over the buffer handling in order to determine song progress, song end etc. So, that leaves me with SDL_AudioCallback in the SDL_AudioSpec API.

I CAN hear the song play, but it’s accompanied by noise and distortion (and sometimes echo depending on what I try).

Setup

var desiredSpec = SDL_AudioSpec()
desiredSpec.freq = Int32(44100)
desiredSpec.format = SDL_AudioFormat(AUDIO_U8)
desiredSpec.samples = 512
desiredSpec.channels = 2
desiredSpec.callback = { callback(userData: $0, stream: $1, length: $2) }

SDL_OpenAudio(&desiredSpec, nil)

Callback

func callback(userData: UnsafeMutableRawPointer?, stream: UnsafeMutablePointer<UInt8>?, length: Int32) {
    // Half sample length to compensate for double byte Int16.
    var output = Array<Int16>.init(repeating: 0, count: Int(length / 2))

    // Generates stereo signed Int16 audio samples.
    gme_play(emulator, Int32(output.count), &output)

    // Converts Int16 samples to UInt8 samples expected by the mixer below. Then adds to buffer.
    var buffer = [UInt8]()
    for i in 0 ..< (output.count) {
        let uIntVal = UInt16(bitPattern: output[i])
        buffer.append(UInt8(uIntVal & 0xff))
        buffer.append(UInt8((uIntVal >> 8) & 0xff))
    }

    SDL_MixAudio(stream, buffer, Uint32(length), SDL_MIX_MAXVOLUME)
}

There are several problems with this code:

  1. Don’t call SDL_OpenAudio(), it’s deprecated. Call SDL_OpenAudioDevice() instead.

  2. You should be specifying AUDIO_S16LSB (or possibly AUDIO_S16MSB if it’s big-endian), not AUDIO_U8.

  3. The code commented as “Converts Int16 samples to UInt8 samples” can be removed, it doesn’t achieve anything (if the source samples are little-endian the output will be identical to the input).

  4. I don’t see any reason to call SDL_MixAudio() (or SDL_MixAudioFormat) since you’re not doing any mixing, don’t need to change the format and are not using it to change the level.

Hi!

Thanks for the reply!

  1. Check.
    1. My original approach was AUDIO_S16LSB when using SDL_QueueAudio. However, since SDL_MixAudio requires UInt8 I needed the conversion (Swift is picky with type).
  2. I have tried simply copying the buffer to the stream via SDL_memcpy, but that gives me mostly static, very little actual melody.

So, I fiddled around with SDL_memcpy a little more and by halving the requested number of samples in the callback (the length param) it actually works! Not sure why though, since format is now set to AUDIO_S16:thinking:

SOLVED!
In the end this was enough to make it work:

var buffer = Array<Int16>(repeating: 0, count: Int(length / 2))
gme_play(emulator, Int32(output.count), &output)
SDL_memcpy(stream, buffer, Int(length))