Prechádzať zdrojové kódy

audio: Don't force minimum device spec.

Now we allow physical devices to reopen at a higher spec if necessary.

Added test/testdynaudioreopen.c to stress test this change.

Fixes #14558.
Ryan C. Gordon 3 mesiacov pred
rodič
commit
36fed09edb
3 zmenil súbory, kde vykonal 195 pridanie a 21 odobranie
  1. 43 21
      src/audio/SDL_audio.c
  2. 1 0
      test/CMakeLists.txt
  3. 151 0
      test/testdynaudioreopen.c

+ 43 - 21
src/audio/SDL_audio.c

@@ -1862,15 +1862,54 @@ static bool OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec
 {
     SerializePhysicalDeviceClose(device);  // make sure another thread that's closing didn't release the lock to let the device thread join...
 
-    if (device->currently_opened) {
-        return true;  // we're already good.
-    }
-
     // Just pretend to open a zombie device. It can still collect logical devices on a default device under the assumption they will all migrate when the default device is officially changed.
     if (SDL_GetAtomicInt(&device->zombie)) {
         return true;  // Braaaaaaaaains.
     }
 
+    SDL_AudioSpec spec;
+    SDL_copyp(&spec, inspec ? inspec : &device->default_spec);
+    PrepareAudioFormat(device->recording, &spec);
+
+    if (device->currently_opened) {
+        SDL_AudioSpec current;
+        SDL_copyp(&current, &device->spec);
+
+        // if something has already opened the device at a lower quality, attempt to reopen it with the new request.
+        // This prevents something intentionally low quality, like VoIP playback, from making the system sound bad
+        // because it opened the hardware before the CD-quality background music arrived. In theory this could cause
+        // an audio hitch, but it would be a one-time thing, and as we've learned from default device migration, not
+        // actually that painful in practice.
+        if ((SDL_AUDIO_BITSIZE(spec.format) <= SDL_AUDIO_BITSIZE(current.format)) && (spec.channels <= current.channels) && (spec.freq <= current.freq)) {
+            return true;  // we're already good.
+        }
+
+        // uhoh, have to reopen the device...choose the "better" values from each spec.
+        spec.format = (SDL_AUDIO_BITSIZE(spec.format) > SDL_AUDIO_BITSIZE(current.format)) ? spec.format : current.format;
+        spec.channels = SDL_max(spec.channels, current.channels);
+        spec.freq = SDL_max(spec.freq, current.freq);
+
+        SDL_Log("AUDIO: attempt to reopen device '%s' at higher spec! (%s,%d,%d => %s,%d,%d)", device->name, SDL_GetAudioFormatName(current.format), current.channels, current.freq, SDL_GetAudioFormatName(spec.format), spec.channels, spec.freq);
+        ClosePhysicalAudioDevice(device);
+        if (!OpenPhysicalAudioDevice(device, &spec)) {
+            // no good, try to go back to our original spec...
+            if (!OpenPhysicalAudioDevice(device, &current)) {
+                // okay, _now_ we're in trouble. Report the device as disconnected, since we've just broken all the existing logical devices. :(
+                // !!! FIXME: the logical devices need a thread to run the zombie implementation.
+                SDL_AudioDeviceDisconnected(device);
+                return false;
+            }
+        }
+
+        // adjust all the attached audio streams to the new format, send format change events, etc.
+        SDL_copyp(&spec, &device->spec);  // save off whatever we ended up with.
+        SDL_copyp(&device->spec, &current);  // put it back to what it was so the next function call doesn't return immediately. The next call will reset it properly.
+        SDL_AudioDeviceFormatChangedAlreadyLocked(device, &spec, device->sample_frames);  // if this fails, it's probably because we're out of memory and didn't send the events, but the device is _probably_ functional!
+
+        return true;  // carry on with the reconfigured device!
+    }
+
+
     // These start with the backend's implementation, but we might swap them out with zombie versions later.
     device->WaitDevice = current_audio.impl.WaitDevice;
     device->PlayDevice = current_audio.impl.PlayDevice;
@@ -1878,23 +1917,6 @@ static bool OpenPhysicalAudioDevice(SDL_AudioDevice *device, const SDL_AudioSpec
     device->WaitRecordingDevice = current_audio.impl.WaitRecordingDevice;
     device->RecordDevice = current_audio.impl.RecordDevice;
     device->FlushRecording = current_audio.impl.FlushRecording;
-
-    SDL_AudioSpec spec;
-    SDL_copyp(&spec, inspec ? inspec : &device->default_spec);
-    PrepareAudioFormat(device->recording, &spec);
-
-    /* We impose a simple minimum on device formats. This prevents something low quality, like an old game using S8/8000Hz audio,
-       from ruining a music thing playing at CD quality that tries to open later, or some VoIP library that opens for mono output
-       ruining your surround-sound game because it got there first.
-       These are just requests! The backend may change any of these values during OpenDevice method! */
-
-    const SDL_AudioFormat minimum_format = device->recording ? DEFAULT_AUDIO_RECORDING_FORMAT : DEFAULT_AUDIO_PLAYBACK_FORMAT;
-    const int minimum_channels = device->recording ? DEFAULT_AUDIO_RECORDING_CHANNELS : DEFAULT_AUDIO_PLAYBACK_CHANNELS;
-    const int minimum_freq = device->recording ? DEFAULT_AUDIO_RECORDING_FREQUENCY : DEFAULT_AUDIO_PLAYBACK_FREQUENCY;
-
-    device->spec.format = (SDL_AUDIO_BITSIZE(minimum_format) >= SDL_AUDIO_BITSIZE(spec.format)) ? minimum_format : spec.format;
-    device->spec.channels = SDL_max(minimum_channels, spec.channels);
-    device->spec.freq = SDL_max(minimum_freq, spec.freq);
     device->sample_frames = SDL_GetDefaultSampleFramesFromFreq(device->spec.freq);
     SDL_UpdatedAudioDeviceFormat(device);  // start this off sane.
 

+ 1 - 0
test/CMakeLists.txt

@@ -353,6 +353,7 @@ endif()
 
 add_sdl_test_executable(checkkeys SOURCES checkkeys.c NAME83 chkkeys)
 add_sdl_test_executable(loopwave NEEDS_RESOURCES TESTUTILS MAIN_CALLBACKS SOURCES loopwave.c)
+add_sdl_test_executable(testdynaudioreopen NEEDS_RESOURCES TESTUTILS MAIN_CALLBACKS SOURCES testdynaudioreopen.c)
 add_sdl_test_executable(testsurround SOURCES testsurround.c NAME83 surround)
 add_sdl_test_executable(testresample NEEDS_RESOURCES SOURCES testresample.c NAME83 resample)
 add_sdl_test_executable(testaudioinfo SOURCES testaudioinfo.c NAME83 audioinf)

+ 151 - 0
test/testdynaudioreopen.c

@@ -0,0 +1,151 @@
+#define SDL_MAIN_USE_CALLBACKS 1
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+static SDL_AudioStream *stream = NULL;
+static Uint8 *wav_data = NULL;
+static Uint32 wav_data_len = 0;
+static SDL_AudioSpec dev_spec;
+static Uint64 next_reopen = 0;
+static int next_spec = 0;
+
+static const SDL_AudioSpec specs[] = {
+    /* mono */
+    { SDL_AUDIO_U8, 1,  8000 }, { SDL_AUDIO_S8, 1,  8000 }, { SDL_AUDIO_S16, 1,  8000 }, { SDL_AUDIO_S32, 1,  8000 }, { SDL_AUDIO_F32, 1,  8000 }, { SDL_AUDIO_F32, 1,  8000 },
+    { SDL_AUDIO_U8, 1, 11025 }, { SDL_AUDIO_S8, 1, 11025 }, { SDL_AUDIO_S16, 1, 11025 }, { SDL_AUDIO_S32, 1, 11025 }, { SDL_AUDIO_F32, 1, 11025 }, { SDL_AUDIO_F32, 1, 11025 },
+    { SDL_AUDIO_U8, 1, 22050 }, { SDL_AUDIO_S8, 1, 22050 }, { SDL_AUDIO_S16, 1, 22050 }, { SDL_AUDIO_S32, 1, 22050 }, { SDL_AUDIO_F32, 1, 22050 }, { SDL_AUDIO_F32, 1, 22050 },
+    { SDL_AUDIO_U8, 1, 44100 }, { SDL_AUDIO_S8, 1, 44100 }, { SDL_AUDIO_S16, 1, 44100 }, { SDL_AUDIO_S32, 1, 44100 }, { SDL_AUDIO_F32, 1, 44100 }, { SDL_AUDIO_F32, 1, 44100 },
+    { SDL_AUDIO_U8, 1, 48000 }, { SDL_AUDIO_S8, 1, 48000 }, { SDL_AUDIO_S16, 1, 48000 }, { SDL_AUDIO_S32, 1, 48000 }, { SDL_AUDIO_F32, 1, 48000 }, { SDL_AUDIO_F32, 1, 48000 },
+    { SDL_AUDIO_U8, 1, 96000 }, { SDL_AUDIO_S8, 1, 96000 }, { SDL_AUDIO_S16, 1, 96000 }, { SDL_AUDIO_S32, 1, 96000 }, { SDL_AUDIO_F32, 1, 96000 }, { SDL_AUDIO_F32, 1, 96000 },
+    /* stereo */
+    { SDL_AUDIO_U8, 2,  8000 }, { SDL_AUDIO_S8, 2,  8000 }, { SDL_AUDIO_S16, 2,  8000 }, { SDL_AUDIO_S32, 2,  8000 }, { SDL_AUDIO_F32, 2,  8000 }, { SDL_AUDIO_F32, 2,  8000 },
+    { SDL_AUDIO_U8, 2, 11025 }, { SDL_AUDIO_S8, 2, 11025 }, { SDL_AUDIO_S16, 2, 11025 }, { SDL_AUDIO_S32, 2, 11025 }, { SDL_AUDIO_F32, 2, 11025 }, { SDL_AUDIO_F32, 2, 11025 },
+    { SDL_AUDIO_U8, 2, 22050 }, { SDL_AUDIO_S8, 2, 22050 }, { SDL_AUDIO_S16, 2, 22050 }, { SDL_AUDIO_S32, 2, 22050 }, { SDL_AUDIO_F32, 2, 22050 }, { SDL_AUDIO_F32, 2, 22050 },
+    { SDL_AUDIO_U8, 2, 44100 }, { SDL_AUDIO_S8, 2, 44100 }, { SDL_AUDIO_S16, 2, 44100 }, { SDL_AUDIO_S32, 2, 44100 }, { SDL_AUDIO_F32, 2, 44100 }, { SDL_AUDIO_F32, 2, 44100 },
+    { SDL_AUDIO_U8, 2, 48000 }, { SDL_AUDIO_S8, 2, 48000 }, { SDL_AUDIO_S16, 2, 48000 }, { SDL_AUDIO_S32, 2, 48000 }, { SDL_AUDIO_F32, 2, 48000 }, { SDL_AUDIO_F32, 2, 48000 },
+    { SDL_AUDIO_U8, 2, 96000 }, { SDL_AUDIO_S8, 2, 96000 }, { SDL_AUDIO_S16, 2, 96000 }, { SDL_AUDIO_S32, 2, 96000 }, { SDL_AUDIO_F32, 2, 96000 }, { SDL_AUDIO_F32, 2, 96000 },
+};
+
+static const char *SpecToString(const SDL_AudioSpec *spec)
+{
+    static char buf[256];
+    SDL_snprintf(buf, sizeof (buf), "%s, %d channel%s, %dHz", SDL_GetAudioFormatName(spec->format), spec->channels, (spec->channels == 1) ? "" : "s", spec->freq);
+    return buf;
+}
+
+static SDL_AudioDeviceID open_device(void)
+{
+    SDL_AudioDeviceID devid = 0;
+
+    while (next_spec < SDL_arraysize(specs)) {
+        const SDL_AudioSpec *spec = &specs[next_spec++];
+
+        if ((SDL_AUDIO_BITSIZE(spec->format) <= SDL_AUDIO_BITSIZE(dev_spec.format)) &&
+            (spec->channels <= dev_spec.channels) &&
+            (spec->freq <= dev_spec.freq)) {
+            continue;  /* don't bother, it won't trigger a reopen. */
+        }
+
+        SDL_Log("Requesting device open at %s", SpecToString(spec));
+        devid = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, spec);
+        if (!devid) {
+            SDL_Log("Couldn't open audio device: %s", SDL_GetError());
+        } else {
+            SDL_GetAudioDeviceFormat(devid, &dev_spec, NULL);
+            SDL_Log("Device actually opened at %s", SpecToString(&dev_spec));
+        }
+
+        next_reopen = SDL_GetTicks() + 2000;
+        break;
+    }
+
+    return devid;
+}
+
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
+{
+    SDL_AudioSpec wav_spec;
+    char *wav_path = NULL;
+    SDL_AudioDeviceID devid = 0;
+
+    /* this doesn't have to run very much, so give up tons of CPU time between iterations. */
+    SDL_SetHint(SDL_HINT_MAIN_CALLBACK_RATE, "5");
+
+    if (!SDL_Init(SDL_INIT_AUDIO)) {
+        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    SDL_Log("Using audio driver: %s", SDL_GetCurrentAudioDriver());
+
+    SDL_asprintf(&wav_path, "%ssample.wav", SDL_GetBasePath());  /* allocate a string of the full file path */
+    if (!SDL_LoadWAV(wav_path, &wav_spec, &wav_data, &wav_data_len)) {
+        SDL_Log("Couldn't load .wav file: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    SDL_free(wav_path);  /* done with this string. */
+
+    devid = open_device();
+    if (!devid) {
+        return SDL_APP_FAILURE;
+    }
+
+    /* Create our audio stream in the same format as the .wav file. It'll convert to what the audio hardware wants. */
+    stream = SDL_CreateAudioStream(&wav_spec, NULL);
+    if (!stream) {
+        SDL_Log("Couldn't create audio stream: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    if (!SDL_BindAudioStream(devid, stream)) {
+        SDL_Log("Couldn't bind audio stream: %s", SDL_GetError());
+        return SDL_APP_FAILURE;
+    }
+
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs when a new event (mouse input, keypresses, etc) occurs. */
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
+{
+    if (event->type == SDL_EVENT_QUIT) {
+        return SDL_APP_SUCCESS;  /* end the program, reporting success to the OS. */
+    }
+    return SDL_APP_CONTINUE;  /* carry on with the program! */
+}
+
+/* This function runs once per frame, and is the heart of the program. */
+SDL_AppResult SDL_AppIterate(void *appstate)
+{
+    /* see if we need to feed the audio stream more data yet.
+       We're being lazy here, but if there's less than the entire wav file left to play,
+       just shove a whole copy of it into the queue, so we always have _tons_ of
+       data queued for playback. */
+    if (SDL_GetAudioStreamQueued(stream) < (int)wav_data_len) {
+        /* feed more data to the stream. It will queue at the end, and trickle out as the hardware needs more data. */
+        SDL_PutAudioStreamData(stream, wav_data, wav_data_len);
+    }
+
+    /* we just open a new logical device and don't bind a stream to it, or even keep
+       a reference to it. We just want to force SDL to reopen the physical device at
+       a higher spec. */
+    if (SDL_GetTicks() >= next_reopen) {
+        if (next_spec < SDL_arraysize(specs)) {
+            open_device();  /* if this fails, that's good data, but keep going. */
+        } else {
+            SDL_Log("No more reopen attempts, we're done!");
+            return SDL_APP_SUCCESS;
+        }
+    }
+
+    return SDL_APP_CONTINUE;
+}
+
+/* This function runs once at shutdown. */
+void SDL_AppQuit(void *appstate, SDL_AppResult result)
+{
+    SDL_free(wav_data);  /* strictly speaking, this isn't necessary because the process is ending, but it's good policy. */
+    /* SDL will clean up the stream and devices for us. */
+}
+