Stereo audio device playing mono

Hi all,

Is there some variation in how audio devices interpret stream data as stereo audio? I.e., are there situations where L and R channels aren’t simply interleaved?

I have one audio device in particular (my airpods) that plays all audio streams I pass to it in mono.

I’ve confirmed that the obtained SDL_AudioSpec shows two channels, and the expected native project audio format (AUDIO_S16LSB). To test, I created an audio stream that should be panned fully to one side, with every other sample given value 0. Other audio devices playing the exact same data play it in stereo.

My audio callback function is below. The implementation of get_mixdown_chunk() shouldn’t be relevant, because the debugging print loop confirms that every other sample is zero.

static void play_callback(void* user_data, uint8_t* stream, int streamLength)
{
    int16_t *chunk = get_mixdown_chunk(proj->tl, streamLength / 2, false);
    /* Printing sample values to confirm that every other sample has value 0 */
    for (uint8_t i = 0; i<200; i++) {
        fprintf(stderr, "%d ", (int16_t)(chunk[i]));
    }
    memcpy(stream, chunk, streamLength);
    free(chunk);
}

Any tips appreciated. Thanks in advance!

  1. Can I get the part of the code where you initialize the audio device? (Please include requested audiospec as you have it initially).

  2. I’ve always seen memcpy used with sizeof(); is the streamLength variable actually reporting the number of bytes in the chunk?

  3. Is the sound that’s being played matching the expected tone/frequency that you expect to hear, or is it possible doubled or halved from the normal frequency? (Does it sound the same as when played on your other audio player?)

  4. We probably should see what the get_mixdown_chunk function is doing. (Though you confirm that it’s providing the proper on-off pattern, so it should not be the issue.)

  1. Sure! Please see below. The AudioDevice struct is initialized by another function that queries available audio devices, and then the name is passed to SDL_OpenAudioDevice() here.
/* Open an audio device and store obtained spec */
int open_audio_device(AudioDevice *device, uint8_t desired_channels)
{
    if (!proj) {
        fprintf(stderr, "Error: opening audio device without an active Project is forbidden.\n");
        exit(1);
    }

    SDL_AudioSpec obtained;

    /* Project determines high-level audio settings */
    device->spec.format = proj->fmt;
    device->spec.samples = proj->chunk_size;
    device->spec.freq = proj->sample_rate;

    device->spec.channels = desired_channels;
    device->spec.callback = device->iscapture ? recording_callback : play_callback;
    device->spec.userdata = device;

    if ((device->id = SDL_OpenAudioDevice(device->name, device->iscapture, &(device->spec), &(obtained), 0)) > 0) {
        device->spec = obtained;
        device->open = true;
    } else {
        device->open = false;
        fprintf(stderr, "Error opening audio device %s : %s\n", device->name, SDL_GetError());
        return -1;
    }
    device->rec_buffer = malloc(BUFFLEN * device->spec.channels);
    return 1;
}

I’m querying and logging the obtained audio spec in the code below. The resulting log confirms that the device has two channels.

    for (uint8_t i=0; i<proj->num_playback_devices; i++) {
        AudioDevice *dev = proj->playback_devices[i];
        fprintf(f, "\t\tDevice at %p:\n", dev);
        fprintf(f, "\t\t\tName: %s:\n", dev->name);
        fprintf(f, "\t\t\tId: %d:\n", dev->id);
        fprintf(f, "\t\t\tOpen: %d:\n", dev->open);
        fprintf(f, "\t\t\tChannels: %d:\n", dev->spec.channels);
        fprintf(f, "\t\t\tFormat: %s:\n", get_audio_fmt_str(dev->spec.format));
        fprintf(f, "\t\t\tWrite buffpos: %d:\n", dev->write_buffpos_sframes);
    }
  1. Yes, it should be – that’s specified by SDL: SDL2/SDL_AudioSpec - SDL Wiki

  2. Good q, yes. The sounds I’m playing are all recorded with a microphone, so I can say pretty confidently that it sounds “correct” in all respects except for the fact that it’s in mono when played back through this one audio device. It all sounds the same on other devices, but in stereo.

  3. This is a can of worms that I’m quite sure won’t be helpful in isolation – the important thing is that the samples printed in the callback function, immediately before they are sent to the audio buffer to be played, look exactly like a stereo signal panned all the way to one side (every other number is 0). E.g. -1675 0 -1698 0 -1703 0 -1685 0 -1665 0 -1647 0 -1638 0 -1626 0 etc. (This is actual output from my program.) But, just in case I’m missing something, here’s that function:

int16_t *get_mixdown_chunk(Timeline *tl, uint32_t len_samples, bool from_mark_in)
{
    uint32_t bytelen = sizeof(int16_t) * len_samples;
    int16_t *mixdown = malloc(bytelen);
    memset(mixdown, '\0', bytelen);
    if (!mixdown) {
        fprintf(stderr, "\nError: could not allocate mixdown chunk.");
        exit(1);
    }

    int i=0;
    float j=0;
    uint32_t start_pos = from_mark_in ? proj->tl->in_mark : proj->tl->play_position * proj->channels;
    while (i < len_samples) {
        for (int t=0; t<tl->num_tracks; t++) {
            mixdown[i] += get_track_sample((tl->tracks)[t], tl, start_pos, (int) j);
        }
        j += from_mark_in ? 1 : proj->play_speed;
        i++;
    }
    proj->tl->play_position += len_samples * proj->play_speed / proj->channels;
    // }
    // fprintf(stderr, "\t->exit get_mixdown_chunk\n");
    return mixdown;
}

Sorry to be nit-picky about information, but have you tested this other headphones to confirm the mono output on besides the airpods? Perhaps something with a hard connection instead of bluetooth?
No, this isn’t it because you get stereo on those airpods on other players, right?

Does your device class follow The Rule of Three? I see a copy assignment being used in your open_audio_device function:

device->spec.userdata = device;

And we are losing scope when the function ends…
Are you using structs or classes? (C or C++)?

I’m sorry, I see now that it’s being passed in by pointer, so that shouldn’t be the problem either.

Yes, the airpods otherwise play stereo fine. It does seem that there’s something funky specifically about airpods, though, since my other devices have no problem. I just found this, which is interesting and seems similar to what I’m experiencing, if not particularly helpful: Logic Pro changing Airpods format. - Apple Community

It’s all in C, so that line is just a struct assignment, which is a copy.

The play_callback function is called by the system any time the audio device needs fresh data. I have to implement the function, but never call it in my code. My implementation just gets the samples to play by calling get_mixdown_chunk(), and copies them over to the device stream. The userdata member of the device spec is a struct (in this case, my AudioDevice struct) that is accessible inside the callback function. (I don’t actually use it now, but will need some of the device info later on)

OK, and the confirmation code (for loop printing all of the device information) shows the correct information even when not run inside the open_audio_device function? (I’m currently suspicious of the “obtained” spec perhaps losing scope). → That data should be maintained by the SDL device itself, so probably another red herring. Sorry.

Yeah, exactly. The obtained struct does cease to exist when that function returns, but only after being copied to device->spec (which is not a pointer), if the device is successfully opened.

I can at least confirm that interleaved data works correctly on my plug-in headphones on my Linux machine.

I wrote a simple test program. It should compile on C and C++ compilers.
It generates a 440Hz tone and blasts it into the left ear. (Turn down your volume to half or so, it only plays at 1/10th of max volume, but it can still be harsh on the ear.)

#include <math.h>

int deviceFreq = 44100;
int toneFreq = 440;
 
float volume = 32765/10;
int channels = 2;

int16_t * genToneLeft(float toneFreq, size_t sec, int *len)
{
	toneFreq = toneFreq/2;

	int bufferSize = channels * sizeof(int16_t) * (deviceFreq * sec);
	int16_t * buffer = (int16_t*) malloc(sizeof(uint16_t) * bufferSize);

	float time = 0;
	if(channels == 1)
	{
    // Mono, plays in both ears, just checking that behavior to be sure.
		for(int s = 0; s < bufferSize; s ++)
		{
			time ++;
			int16_t value = (int16_t) (volume * cos(time * toneFreq));
			buffer[s] = value;
		}
	}
	else if(channels == 2)
	{
		for(int s = 0; s+1 < bufferSize; s += 2)
		{
			time ++;
			int16_t value = (int16_t) (volume * cos(time * toneFreq));
			buffer[s] = value;
			buffer[s + 1] = 0;
		}
	}
	*len = bufferSize;
	return buffer;
}

SDL_AudioDeviceID initAudioDev()
{
	SDL_AudioSpec spec;
	SDL_zero(spec);
	spec.freq = deviceFreq;
	spec.format = AUDIO_S16LSB;
	spec.channels = channels;
	spec.samples = 4096;
	spec.callback = 0;
	return SDL_OpenAudioDevice(NULL, 0, &spec, &spec, 0);

}

int main()
{
	SDL_Init(SDL_INIT_EVERYTHING);
	SDL_AudioDeviceID deviceID = initAudioDev();

	int length = 0;
	int16_t * buffer = genToneLeft(toneFreq, 5, &length);
	SDL_QueueAudio(deviceID, buffer, length);
	SDL_PauseAudioDevice(deviceID, SDL_FALSE);

	SDL_Delay(5000);

	free(buffer);

	SDL_CloseAudioDevice(deviceID);
	SDL_Quit();
}

I am compiling using:

gcc main.c -lSDL2 -lm

If this does work correctly for you, then my key difference is using SDL_QueueAudio() to queue the data instead of using a callback. This would probably point the problem at the callback being used, which points at that memcpy again.

If it does not work properly, then we can almost definitely say that it’s the operating system or hardware causing the trouble. If that’s the case, then it’s time to report the issue on the SDL Github page for dev support.

1 Like

Thank you for taking the time to write up this test example! It does work as it should, even on my airpods. To check whether the issue is in the use of a callback function rather than queuing audio, I refactored your example to use the audio callback method and a memcpy, similar to what I have in my application:

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "SDL.h"

#define CHUNK_SIZE 4096
#define CHANNELS 2
int deviceFreq = 44100;
int toneFreq = 440;
 
float volume = 32765/10;


int16_t buffer[CHUNK_SIZE * CHANNELS];

static void audio_callback(void* user_data, uint8_t* stream, int streamLength) 
{
    /* Initialize stream to silence */
    memset(stream, '\0', streamLength);

    /* Copy tone to stream */
    memcpy(stream, buffer, streamLength);
}

void genToneLeft(float toneFreq)
{
	toneFreq = toneFreq/2;

	float time = 0;
	if(CHANNELS == 1)
	{
    // Mono, plays in both ears, just checking that behavior to be sure.
		for(int s = 0; s < CHUNK_SIZE * CHANNELS; s ++)
		{
			time ++;
			int16_t value = (int16_t) (volume * cos(time * toneFreq));
			buffer[s] = value;
		}
	}
	else if(CHANNELS == 2)
	{
		for(int s = 0; s+1 < CHUNK_SIZE * CHANNELS; s += 2)
		{
			time ++;
			int16_t value = (int16_t) (volume * cos(time * toneFreq));
            buffer[s] = value;
            buffer[s + 1] = 0;
		}
	}
}

SDL_AudioDeviceID initAudioDev()
{
	SDL_AudioSpec spec;
	SDL_zero(spec);
	spec.freq = deviceFreq;
	spec.format = AUDIO_S16LSB;
	spec.channels = CHANNELS;
	spec.samples = CHUNK_SIZE;
	spec.callback = audio_callback;
	return SDL_OpenAudioDevice(NULL, 0, &spec, &spec, 0);
}

int main()
{
	SDL_Init(SDL_INIT_EVERYTHING);
	SDL_AudioDeviceID deviceID = initAudioDev();

    genToneLeft(toneFreq);

	SDL_PauseAudioDevice(deviceID, SDL_FALSE);

	SDL_Delay(2000);

	// free(buffer);

	SDL_CloseAudioDevice(deviceID);
	SDL_Quit();
}

Much to my surprise, this plays in the left ear on my airpods, as it should! (The audio skips a bit due to phase issues in the sine tone, but that’s expected.)

So that confirms that there must be a bug in my application code somewhere. Even thought I’ve checked and re-checked the device spec and that the memcpy in the callback produces what looks like a stereo signal in the stream, I’m leaning back toward your original suspicion, that there must be something fishy in the way I’m handling the audio device. I’ll keep digging and, with any luck, will post here when I find the problem!

Glad it’s not a hardware issue, those can stop a project dead in its tracks.
Good luck!

1 Like

After losing my mind, I was able to isolate the problem. It’s just a limitation of AirPods (and perhaps other bluetooth devices). The problem occurs when the AirPods are opened as both a playback device and a record device. Opening them as a record device forces the playback to mono.

The behavior is identical any time the AirPods are opened for recording by any other application. I tested this in quicktime and audacity.

I was hung up on the fact that SDL treats the playback instance of the AirPods as a distinct device (with a distinct id) from the record instance, and the specs don’t share any memory – but of course, SDL and the sound card don’t have authority over what the AirPods want to do.

I don’t have a device to test with, but I realize this might also be true for audio jack plug-in headsets, since a conventional 1/8" TRS connectors can only carry two analog signals at a time.

The solution for me is probably to open the default recording device only when I need it, and then close it when I’m done. There’d be an associated performance tradeoff, but probably worth it.

Hope this comes in handy to anyone else who runs into the same problem. Here’s the code I used to confirm the hypothesis:

    char *default_record_device_name;
    char *default_playback_device_name;

    SDL_AudioSpec r_spec;
    SDL_AudioSpec p_spec;

    SDL_GetDefaultAudioInfo(&default_record_device_name, &r_spec, 1);
    SDL_GetDefaultAudioInfo(&default_playback_device_name, &p_spec, 0);

    fprintf(stderr, "Default record device name: %s, spec channels: %d\n", default_record_device_name, r_spec.channels);
    fprintf(stderr, "Default playback device name: %s, spec channels: %d\n", default_playback_device_name, p_spec.channels);

    // p_spec.freq = 44100;
    // p_spec.format = AUDIO_S16LSB;
    // p_spec.channels = CHANNELS;
    // p_spec.samples = CHUNK_SIZE;
    // p_spec.callback = audio_callback;
    // ^ These lines have no effect on the problem

    SDL_AudioSpec r_obtained;
    SDL_AudioSpec p_obtained;

    fprintf(stderr, "Opening playback device in ");
    for (uint8_t i=3; i>0; i--) {
        fprintf(stderr, "%d...", i);
        SDL_Delay(1000);
    }
    fprintf(stderr, "\n");
	SDL_AudioDeviceID playback_id = SDL_OpenAudioDevice(default_playback_device_name, 0, &p_spec, &p_obtained, 0);
    fprintf(stderr, "Opening record device in ");
    for (uint8_t i=3; i>0; i--) {
        fprintf(stderr, "%d...", i);
        SDL_Delay(1000);
    }
    fprintf(stderr, "\n");

    /* This line forces mono playback on AirPods, both in SDL programs and other applications */
    SDL_AudioDeviceID record_id = SDL_OpenAudioDevice(default_record_device_name, 1, &r_spec, &r_obtained, 0);

    fprintf(stderr, "Record device opened\n");
    SDL_Delay(1000);

    fprintf(stderr, "Default record device id: %d, obtained spec channels: %d\n", record_id, r_obtained.channels);
    fprintf(stderr, "Default playback device id: %d, obtained spec channels: %d\n", playback_id, p_obtained.channels);
1 Like