Fully committing to it...!
This left SDL_wave.* alone for now, since there's a ton of comments in there
and this code hasn't changed much from SDL2 so far. But as SDL2 ages out a
little more, I'll likely switch this over, too.
First stage happens before we destroy objects, and is generally used to
shut down hotplug. The second stage is the usual deinit, which cleans up
the lowlevel API, unloads shared libraries, etc.
- No more tapdance to either join the audio device thread or have it detach
itself. Significant simplication of and fixes to the locking code to prevent
deadlocks.
- Physical devices now keep a refcount. Each logical device increments it,
as does the existence of a device thread, etc. Last unref destroys the
device and takes it out of the device_hash. Since there's a lot of moving
parts that might be holding a reference to a physical device, this seemed
like a safer way to protect the object.
- Disconnected devices now continue to function as zombie devices. Playback
devices will still consume data (and just throw it away), and capture devices
will continue to produce data (which always be silence). This helps apps
that don't handle disconnect events; the device still stops playing/capturing,
but bound audio streams will still consume data so they don't allocate more
data infinitely, and apps that depend on an audio callback firing regularly
to make progress won't hang.
Please note that disconnected audio devices must now be explicitly closed!
They always _should_ have been, but before this commit, SDL3 would destroy the
disconnected device for you (and manually closing afterwards was a safe no-op).
Reference Issue #8331.
Fixes#8386.
(and probably others).
All devices are in a single hash, whether playback or capture, or physical
or logical. Lookups are keyed on device ID and map to either
`SDL_AudioDevice *` for physical devices or `SDL_LogicalAudioDevice *` for
logical devices (as an implementation detail, you can determine which object
type you have by checking a specific bit in the device ID).
This simplifies a bunch of code, makes some cases significantly more
efficient, and solves the problem of having to lock each physical
device while the device list rwlock is held to find logical devices by ID.
Device IDs hash perfectly evenly, too, being incrementing integers.
The following objects now have properties that can be user modified:
* SDL_AudioStream
* SDL_Gamepad
* SDL_Joystick
* SDL_RWops
* SDL_Renderer
* SDL_Sensor
* SDL_Surface
* SDL_Texture
* SDL_Window
We need to do this early in the file, so that it will be taken into
account when deciding whether to define NEED_SCALAR_CONVERTER_FALLBACKS
and therefore provide a non-SIMD fallback.
Mitigates: https://github.com/libsdl-org/SDL/issues/8352
Signed-off-by: Simon McVittie <smcv@collabora.com>
This is an attempt to centralize all the error handling, instead of
implicitly counting on WaitDevice implementations to disconnect the device
to report an error.
This should retry until GetCurrentPosition succeeds. Otherwise, we would be
going on to the next iteration too soon.
Also generally streamlined the code while I was in here.
This prevents catastrophe if someone tries to close the device in an event
filter in response to the event.
Note that this means SDL_GetAudioStreamDevice() for any stream on this
device will return 0 during the event filter!
Fixes#8331.
Android claims to work with multiple devices, but doesn't actually appear to
(at least, afaict), and it will report tons of devices that all just seem
to play to the current default output, so for now, turn this off and only
expose a default device.
And then, with that default output, attempt to recover on errors by throwing
away the current AAudioStream and building a new one.
This let me plug/unplug a set of headphones from the headphone jack and audio
would switch correctly to the new output.
Now we sleep the thread in WaitDevice until ALSA reawakens it because it
needs more data, and we feed it exactly as much as it can take at that
point.
Like the recent PulseAudio changes, this is both more efficient, reliable,
and consistent.
In practice, this seems to buffer a little upfront and then gives a pretty
consistent request flow after that of 1/4 of the requested buffer size without
variation, which is significantly better than the previous code that would
vary a little each frame.
Plus, as long as the device asks for _anything_, we won't block forever, and
if it asks for more than our expected buffer size, we'll run multiple times
to satisfy it, so this is likely more robust against dropouts in general, too.
This reverts commit 6fd0613ac8.
Turns out that the Steam Runtime is still on PulseAudio 1.1, and the only
thing we (currently) need a newer Pulse for is pa_threaded_mainloop_set_name,
so let's just go back to treating that symbol as optional.
We might need to force a higher version at some point, but it's not worth it
over this.
Otherwise, we get into situations where all bound streams need to change
their output formats when a device pauses...and it makes the fast case
slow: when pausing a single input, it needs to silence and then convert a
silent buffer, instead of just zeroing out the device buffer and being done.
Since these get proxied to a different thread, if we wait for that thread
to finish while holding the lock, and the management thread _also_ requests
the lock, we're screwed.
WaitDevice never holds the lock by design, so just mark devices as failed
and clean up or recover them in there.
The audio processing thread isn't scheduled in lock-step with the audio callback so sometimes the callback would consume the same data twice and sometimes the audio processing thread would write to the same buffer twice.
Also handle variable sizes in the audio callback so the Android audio system doesn't have to do additional buffering to match our buffer size requirements.
Saves locks and copies during audio thread iteration. We've added asserts
that can evaporate out in release mode to make sure everything stays in sync.
This fires if an opened device changes formats (which it can on Windows,
if the user changes this in the system control panel, and WASAPI can
report), or if a default device migrates to new hardware and the format
doesn't match.
This will fire for all logical devices on a physical device (and if it's
a format change and not a default device change, it'll fire for the
physical device too, but that's honestly not that useful and might change).
Fixes#8267.
("preconverted bytes" makes it sounds like we already converted them before
the call instead of "bytes that haven't yet hit the stage where we convert
them. Just dump the wording completely.)
Now it offers the total requested bytes in addition to the amount
immediately needed (and immediately needed might be zero if the stream
already has enough queued to satisfy the request.
You can see it in action in testaudio by mousing over a logical device; it
will show a visualizer for the current PCM (whatever is currently being
recorded on a capture device, or whatever is being mixed for output on
playback devices).
Fixes#8122.
This is adds complexity and fragility for small optimization wins.
The biggest win is the extremely common case of a single stream providing
the only output, so we'll check for that and skip silencing/mixing/converting.
Otherwise, just use a single mixer path.
This only does this work if actually mixing; if the physical device only
has a single stream bound to it, it'll just write the data to the hardware
without the extra drama.
Fixes#8123.
Currently it's SILENCE (just zero out the mix buffer), COPYONE (one stream
writes directly into the hardware's buffer), or MIX (everything gets mixed
together before sending to the hardware).
Devices that aren't doing anything result in SILENCE. Devices playing
one thing result in COPYONE.
This lets the two most common states take what are likely significantly
faster approaches.
There will likely be some other strategies later (like when we offer a
postmix callback, etc).
This is meant to offer a simplified API for people that are either migrating
directly from SDL2 with minimal effort or just want to make noise without
any of the fancy new API features.
Users of this API can just deal with a single SDL_AudioStream as their only
object/handle into the audio subsystem.
They are still allowed to open multiple devices (or open the same device
multiple times), but cannot change stream bindings on logical devices opened
through this function.
Destroying the single audio stream will also close the logical device behind
the scenes.
Since the top-level table is getting undefined, all the things in it will
be unreachable and eligible for garbage collection without explicitly
nulling them out.
Now, if the AudioContext starts in a "suspended" state, because the browser
blocked it from playing by default, we we run the audio "thread" in a timer
and throw away the generated audio. Once the AudioContext is allowed to
resume, we clear this timer.
The end result is that the app will continue to drain its audio queue
instead of consuming more memory over time (and, if it relies on an audio
callback to make progress, continue to run!), with the effect that the
page is merely silent but otherwise functioning as intended.
Once the user interacts with the page and the browser permits the the
AudioContext to run for real, audio should still be in sync, instead of
just starting to play audio that might now be at least several seconds behind.
Some of the SDL_Convert_F32_to_*_SSE2 do not explicitly clamp the input,
but instead rely on saturating casts.
Inputs very far outside the valid [-1.0, 1.0] range may produce
an incorrect result, but I believe that is an acceptable trade-off.
This usually manifests as a clicking sound, because it often produces
a value outside the range -1.0f to 1.0f based on whatever random data
is past the buffer, which later stages of audio conversion will clamp
to a maximum value for the audio format. Since this tends to be a single
bad sample generated at the end of the resampled buffer, it sounds like
a repeating click in streamed data.
I'd like a more efficient means to clamp this value to not overflow the
buffer, but this puts out the immediate fire.
The current status is stored in the SDL_rwops 'status' field to be able to determine whether a 0 return value is caused by end of file, an error, or a non-blocking source not being ready.
The functions to read sized datatypes now return SDL_bool so you can detect read errors.
Fixes https://github.com/libsdl-org/SDL/issues/6729
This is allegedly lower-latency than the AAudioStream_write interface,
but more importantly, it let me set this up to block in WaitDevice.
Also turned on the low-latency performance mode, which trades battery life
for a more efficient audio thread to some unspecified degree.
This involved moving an `#ifdef` out of SDL_audio.c for thread priority,
so the default ThreadInit now does the usual stuff for non-Android platforms,
the Android platforms provide an implementatin of ThreadInit with their
side of the `#ifdef` and other platforms that implement ThreadInit
incorporated the appropriate code...which is why WASAPI is touched in here.
The Android bits compile, but have not been tested, and there was some
reworkings in the Java bits, so this might need some further fixes still.
I have no way to compile or test this atm, so this will likely need
further attention. I ended up cleaning this up a ton and adding missing
features, so the code changes are pretty dramatic vs a simple conversion
to SDL3...so tread carefully in here.
This does an enormous amount of work in SDL_immdevice.c to simplify and
clean up that interface, while moving some of its responsibilities to the
higher level SDL_audio.c. I hope I saw the whole picture here, and this
wasn't foolhardy of me.
WASAPI has not been updated for these changes, or for SDL3 at all, yet. As
such, it continues to be broken for now. It will be updated soon.
This code compiles with my cross compiler, but hasn't been built with
Visual Studio, or tested in any form, so there might be obvious fixes
following along shortly.
Every single case of this didn't want the device locked, so just looking
it up without having to immediately unlock it afterwards is better here.
Often these devices are passed on to other functions that want to lock them
themselves anyhow (disconnects, default changes, etc).
However, this still blocks in PlayDevice and leaves WaitDevice as a no-op,
which isn't ideal, since the device lock is held during PlayDevice.
Ideally, this should be fixed.
We were firing a semaphore from the JACK-provided thread to otherwise work
within the standard SDL2 device thread, but there's no need for this in SDL3.
This would happen when you had ~1 frame of audio left in the stream, and
resampling needs would cause this to not be enough to produce audio.
But since we're already flushed, we can just add silence padding to let the
app extract these last bits.
Before it would just block in read operations, but separating this out
matches what output devices already do, and also lets us separate out the
unlocked waiting part from the fast part that holds the device lock.
This means "I don't care what format I get at all" and will just use
the device's current (and/or default) format.
This can be useful, since audio streams cover the differences anyhow.
If we wait for context subscription to finish, we might miss the signal
telling us to terminate the thread...this can happen if an app initializes
the audio subsystem and then quits immediately.
So just go right into the main loop of the thread; the subscription will
finish when it finishes and then events will flow.
Zombie devices just sit there doing nothing until a new default device
is chosen, and then they migrate all their logical devices before being
destroyed.
This lets the system deal with the likely outcome of a USB headset being
the default audio device, and when its cable is yanked out, the backend
will likely announce this _before_ it chooses a new default (or, perhaps,
the only device in the system got yanked out and there _isn't_ a new
default to be had until the user plugs the cable back in).
This lets the audio device hold on without disturbing the app until it can
seamlessly migrate audio, and it also means the backend does not have to
be careful in how it announces device events, since SDL will manage the
time between a device loss and its replacement.
Note that this _only_ applies to things opened as the default device
(SDL_AUDIO_DEVICE_DEFAULT_OUTPUT, etc). If those USB headphones are the
default, and one SDL_OpenAudioDevice() call asked for them specifically and
the other just said "give me the system default," the explicitly requested
open will get a device-lost notification immediately. The other open will
live on as a zombie until it can migrate to the new default.
This drops the complexity of the PulseAudio hotplug thread dramatically,
back to what it was previously, since it no longer needs to fight against
Pulse's asychronous nature, but just report device disconnects and new
default choices as they arrive.
loopwave has been updated to not check for device removals anymore; since
it opens the default device, this is now managed for it; it no longer
needs to close and reopen a device, and as far as it knows, the device
is never lost in the first place.
These files are completely different from SDL2, and no clean merging
is likely to happen there anyhow, so there's really no harm in just
switching them over completely to SDL3's new policy of allowing `//`
comments and mixed variable declarations.
Feels deeply sacrilegious, though.
Now you open an audio device and attach streams, as planned, but each
open generates a new logical device. Each logical device has its own
streams that are managed as a group, but all streams on all logical
devices are mixed into a single buffer for a single OS-level open of
the physical device.
This allows multiple opens of a device that won't interfere with each
other and also clean up just what the opener assigned to their logical
device, so all their streams will go away on close but other opens will
continue to mix as they were.
More or less, this makes things work as expected at the app level, but
also gives them the power to group audio streams, and (once added) pause
them all at once, etc.
I don't think this can fail at the moment, but if WaveCheckFormat goes
out of sync with this switch statement at some point, this seems like
a good failsafe.
- Make sure the hotplug thread has hit its main loop before letting
DetectDevices continue.
- Don't unref the context subscription operation until it completes
(or we are shutting down).
I'm not sure which change fixed the problem, but at least one of them
appears to have done so.
Reference Issue #7971.
(cherry picked from commit b9d16dac4e)
Now the operation state change we're waiting on will signal the
threaded mainloop, so this doesn't wait longer than necessary.
This requires PulseAudio 4.0 or later, so don't merge this into SDL2,
which requires PulseAudio 0.9.15.
Fixes#7971.
This risks blocking the thread if disaster ensues, and we can wait in the
thread's main loop for subscription as well anywhere else.
Reference Issue #7971.
If SDL_HINT_APP_ID is set, pass it as the application.id to pipewire.
This gives any pipewire-based tools a hint to find an associated
.desktop file for icons, etc.
We weren't meant to have multiple contexts and mainloops, but we had one
for each opened device and the hotplug detection thread. Instead, use
pa_threaded_mainloop, which can be shared between threads and objects, and
a single context (which, according to the PulseAudio documentation, is
usually meant to be a singleton that represents a global server connection,
possibly with multiple streams hung on it).
Now instead of polling in a loop, threads will block until the
threaded_mainloop runs a callback, and the callback will fire a signal to
unblock the thread.
Prior to this, the code upset ThreadSanitizer, as Pulse has some unprotected
global resource that each mainloop/context would touch.
Reference Issue #7427.
SDL mutexes are always recursive in modern times, so no need to check this,
plus the test triggers a false-positive on ThreadSanitizer.
Reference Issue #7427.
In theory this is illegal, but legit wavefiles in the field do it, and
it's easy to bump it to 1 for general purposes.
Formats with more specific alignment requirements already check for them
separately.
Fixes#7714.
This was only including the resampling buffer needs if it was larger
the other allocation needs, but it needs to be included unconditionally.
For safety's sake, we also make sure the pre-resample buffer doesn't risk
overflow, too, but this might not be necessary in practice.
Before, as ConvertAudio might have expanded data in-place temporarily during
its work, this could blow up. Now if there's a chance of that, it'll
work out of an internal buffer and copy the final results to the output
buffer.
If the output format can handle the temporary expansion, we write directly
to the output buffer without the extra copy.
Fixes#7668.