3#include <SDL3_mixer/SDL_mixer.h>
31 std::atomic<bool> reset {
true };
32 std::array<float, MAX_FILTER_CHANNELS> state {};
36 MIX_Track* track =
nullptr;
37 uint32_t generation = 0;
39 bool positional =
false;
47 float occlusion = 0.0F;
52 float effective_gain = 0.0F;
76std::array<float, NUM_BUSES>
bus_volume = { 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
77std::array<Duck, NUM_BUSES>
ducks {};
98 const auto i =
static_cast<size_t>(bus);
104 return v.
track !=
nullptr && (MIX_TrackPlaying(v.
track) || MIX_TrackPaused(v.
track));
108 void* userdata, MIX_Track* ,
const SDL_AudioSpec* spec,
float* pcm,
int samples)
110 auto* filter =
static_cast<LowPass*
>(userdata);
111 const int channels = spec->channels;
116 auto& state = filter->
state;
117 if (filter->reset.exchange(
false)) {
121 const int frames = samples / channels;
122 const float cutoff = filter->cutoff_hz.load(std::memory_order_relaxed);
127 for (
int c = 0; c < channels; ++c) {
128 state[c] = pcm[((frames - 1) * channels) + c];
133 const float alpha = 1.0F
134 - std::exp((-2.0F * SDL_PI_F * cutoff) /
static_cast<float>(std::max(1, spec->freq)));
136 for (
int f = 0; f < frames; ++f) {
137 for (
int c = 0; c < channels; ++c) {
138 float& sample = pcm[(f * channels) + c];
139 state[c] += alpha * (sample - state[c]);
148 const float far = std::max(att.
max_distance, near + 0.001F);
150 if (distance <= near) {
153 if (distance >= far) {
161 return 1.0F - ((distance - near) / (far - near));
164 const float floor = near / far;
165 return ((near / distance) - floor) / (1.0F - floor);
169 const float floor = (near * near) / (far * far);
170 return (((near * near) / (distance * distance)) - floor) / (1.0F - floor);
185 const float distance = to_listener.
magnitude();
186 if (distance < 0.001F) {
190 const auto dir = to_listener / distance;
192 const float listener_speed
202 pan = std::clamp(pan, -1.0F, 1.0F);
212 MIX_StereoGains gains;
213 gains.left = std::sqrt((1.0F - pan) * 0.5F);
214 gains.right = std::sqrt((1.0F + pan) * 0.5F);
215 MIX_SetTrackStereo(v.
track, &gains);
229 const MIX_Point3D point { x, 0.0F, z };
230 MIX_SetTrack3DPosition(v.
track, &point);
237 float ratio = v.
pitch;
241 const float distance = delta.
magnitude();
248 if (distance > 0.001F) {
249 set_point(v, delta.x / distance, delta.y / distance);
267 std::memory_order_relaxed);
275 ratio = std::clamp(ratio, 0.01F, 100.0F);
278 MIX_SetTrackFrequencyRatio(v.
track, ratio);
295 for (
size_t i = 0; i <
voices.size(); ++i) {
296 const auto& v =
voices[i];
297 if (v.track ==
nullptr) {
301 return static_cast<int>(i);
303 if (v.priority > priority) {
307 if (best < 0 || v.priority <
voices[best].priority
308 || (v.priority ==
voices[best].priority
309 && v.effective_gain <
voices[best].effective_gain)) {
310 best =
static_cast<int>(i);
316float vary(
float value,
float variation)
318 if (variation <= 0.0F) {
331 , generation(generation)
339 if (voice < 0 ||
static_cast<size_t>(voice) >=
voices.size()) {
344 if (v.generation != generation || !
is_active(v)) {
353 return resolve(voice, generation) !=
nullptr;
358 if (
auto* v = resolve(voice, generation)) {
359 MIX_StopTrack(v->track,
360 MIX_TrackMSToFrames(v->track,
static_cast<Sint64
>(fade_out_s * 1000.0F)));
366 if (
auto* v = resolve(voice, generation)) {
367 MIX_PauseTrack(v->track);
373 if (
auto* v = resolve(voice, generation)) {
374 MIX_ResumeTrack(v->track);
380 if (
auto* v = resolve(voice, generation)) {
381 v->volume = std::clamp(volume, 0.0F, 1.0F);
388 if (
auto* v = resolve(voice, generation)) {
396 if (
auto* v = resolve(voice, generation)) {
397 v->positional =
false;
405 if (
auto* v = resolve(voice, generation)) {
406 v->positional =
true;
407 v->position = position;
414 if (
auto* v = resolve(voice, generation)) {
415 v->velocity = velocity;
422 if (
auto* v = resolve(voice, generation)) {
423 v->occlusion = std::clamp(amount, 0.0F, 1.0F);
430 if (
auto* v = resolve(voice, generation)) {
431 v->attenuation = attenuation;
452 for (
auto& v : voices) {
455 music_track =
nullptr;
468 if (mixer !=
nullptr) {
480 spec.format = SDL_AUDIO_S16LE;
484 mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);
485 if (mixer ==
nullptr) {
490 for (
auto& v : voices) {
491 v.track = MIX_CreateTrack(mixer);
493 if (v.track ==
nullptr) {
497 MIX_SetTrackCookedCallback(v.track, low_pass_callback, &v.filter);
500 music_track = MIX_CreateTrack(mixer);
501 music_sent_gain = -1.0F;
502 if (music_track ==
nullptr) {
507 last_update_ns = SDL_GetTicksNS();
513 if (mixer ==
nullptr) {
517 const uint64_t now = SDL_GetTicksNS();
518 const float dt =
static_cast<float>(now - last_update_ns) / 1'000'000'000.0F;
519 last_update_ns = now;
522 for (
auto& d : ducks) {
524 if (d.hold_s > 0.0F) {
531 const float step = dt / std::max(d.fade_s, 0.001F);
532 d.gain = d.gain < target ? std::min(d.gain + step, target) : std::max(d.gain - step, target);
535 for (
auto& v : voices) {
546 static SoundHandle make(
int voice, uint32_t generation) {
return { voice, generation }; }
555 if (
mixer ==
nullptr || sample ==
nullptr ||
static_cast<size_t>(options.
bus) >=
NUM_BUSES) {
566 MIX_StopTrack(v.track, 0);
570 v.positional = position !=
nullptr;
571 v.position = position !=
nullptr ? *position :
asw::Vec2<float>(0.0F, 0.0F);
580 v.filter.reset.store(
true);
584 MIX_SetTrackAudio(v.track, sample.get());
587 const SDL_PropertiesID props = SDL_CreateProperties();
588 SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, options.
loop ? -1 : 0);
590 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER,
591 static_cast<Sint64
>(options.
fade_in_s * 1000.0F));
593 const bool played = MIX_PlayTrack(v.track, props);
594 SDL_DestroyProperties(props);
606 return start(sample, options,
nullptr);
610 const asw::Sample& sample,
float volume,
float pan,
bool loop)
616 return play(sample, options);
623 const float distance = (position - listener_position).magnitude();
624 if (!options.
loop && attenuate(options.
attenuation, distance) <= 0.0F) {
628 return start(sample, options, &position);
635 return play_at(sample, x, options);
643 return play(sample, options);
647 const float center = width / 2.0F;
648 const float offset = (x - center) / center;
652 const float falloff = std::clamp((3.0F - std::abs(offset)) / 2.0F, 0.0F, 1.0F);
653 if (falloff <= 0.0F) {
658 panned.
volume = std::clamp(options.
volume * falloff, 0.0F, 1.0F);
659 panned.
pan = std::clamp(offset * 0.7F, -1.0F, 1.0F);
660 return play(sample, panned);
665 listener_position = position;
666 listener_velocity = velocity;
671 return listener_position;
681 doppler_factor = std::max(0.0F, factor);
682 speed_of_sound = std::max(1.0F, speed);
690 bus_volume[
static_cast<size_t>(bus)] = std::clamp(volume, 0.0F, 1.0F);
698 return bus_volume[
static_cast<size_t>(bus)];
707 auto& d = ducks[
static_cast<size_t>(bus)];
708 d.target = std::min(d.hold_s > 0.0F ? d.target : 1.0F, std::clamp(gain, 0.0F, 1.0F));
709 d.hold_s = std::max(d.hold_s, hold_s);
710 d.fade_s = std::max(0.0F, fade_s);
715 music_base_volume = std::clamp(volume, 0.0F, 1.0F);
717 MIX_SetTrackAudio(music_track, sample.get());
719 const SDL_PropertiesID options = SDL_CreateProperties();
720 SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
721 SDL_SetNumberProperty(
722 options, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER,
static_cast<int>(fade_in_s * 1000.0F));
723 MIX_PlayTrack(music_track, options);
724 SDL_DestroyProperties(options);
730 const auto fade_out_frames
731 = MIX_TrackMSToFrames(music_track,
static_cast<Sint64
>(fade_out_s * 1000.0F));
732 MIX_StopTrack(music_track, fade_out_frames);
737 MIX_PauseTrack(music_track);
742 MIX_ResumeTrack(music_track);
747 return MIX_TrackPlaying(music_track);
752 return MIX_TrackPaused(music_track);
757 master_volume = std::clamp(volume, 0.0F, 1.0F);
758 MIX_SetMixerGain(mixer, master_volume);
774 return master_volume;
T x
The x component of the vector.
Real magnitude() const
Calculate the magnitude of the vector.
T dot(const Vec2 &other) const
Calculate the dot product of two vectors.
A handle to a playing sound, used to change it while it plays.
void set_occlusion(float amount) const
Muffle the sound, as if heard through a wall.
void set_velocity(const Vec2< float > &velocity) const
Set how fast the sound is moving, for doppler pitch shift.
void set_attenuation(const Attenuation &attenuation) const
Change the distance fall off.
void set_position(const Vec2< float > &position) const
Set where the sound is in the world. Makes the sound positional.
bool is_playing() const
Check if the sound is still playing or paused.
void stop(float fade_out_s=0.0F) const
Stop the sound.
void set_pitch(float pitch) const
Set the playback speed and pitch.
SoundHandle()=default
Create a handle that refers to nothing.
void pause() const
Pause the sound.
void set_pan(float pan) const
Set the panning. Makes a positional sound non positional.
void resume() const
Resume a paused sound.
void set_volume(float volume) const
Set the volume.
Display and window routines for the ASW library.
Structured logging system.
constexpr float OCCLUDED_GAIN
constexpr int MAX_FILTER_CHANNELS
constexpr float MAX_DOPPLER
std::array< float, NUM_BUSES > bus_volume
void SDLCALL low_pass_callback(void *userdata, MIX_Track *, const SDL_AudioSpec *spec, float *pcm, int samples)
constexpr float MIN_DOPPLER
void set_stereo_pan(Voice &v, float pan)
float bus_gain(asw::sound::Bus bus)
constexpr float OCCLUDED_CUTOFF_HZ
asw::sound::SoundHandle start(const asw::Sample &sample, const asw::sound::PlayOptions &options, const asw::Vec2< float > *position)
std::array< Voice, NUM_VOICES > voices
constexpr size_t NUM_BUSES
bool is_active(const Voice &v)
std::array< Duck, NUM_BUSES > ducks
asw::Vec2< float > listener_position
asw::sound::SpatialMode spatial_mode
Voice * resolve(int voice, uint32_t generation)
int find_voice(int priority)
float attenuate(const asw::sound::Attenuation &att, float distance)
float doppler(const Voice &v)
asw::Vec2< float > listener_velocity
void set_point(Voice &v, float x, float z)
constexpr float OPEN_CUTOFF_HZ
constexpr size_t NUM_VOICES
float vary(float value, float variation)
asw::Vec2< int > get_logical_size()
Get the logical size of the window. This may differ from the actual size if scaling is enabled.
void error(const std::string &message)
Log an error message.
void warn(const std::string &message)
Log a warning message.
int between(int min, int max)
Generate a random integer between min and max.
bool is_music_paused()
Check if music is paused.
void set_doppler(float factor, float speed_of_sound=3000.0F)
Set how strong doppler pitch shift is.
void pause_music()
Pause the currently playing music.
float get_master_volume()
Get the current master volume multiplier.
void play_music(const asw::Music &sample, float volume=1.0F, float fade_in_s=0.0F)
Play a music sample.
bool is_music_playing()
Check if music is currently playing.
void set_sfx_volume(float volume)
Set the SFX volume multiplier. Same as the Sfx bus volume.
float get_music_volume()
Get the current music volume multiplier.
void resume_music()
Resume paused music.
Bus
Mix buses. Each sound plays on one bus, and each bus has its own volume and can be ducked.
void _shutdown()
Shut down the sound module. Called automatically by asw::core::shutdown().
MIX_Mixer * get_mixer()
Get the SDL mixer device.
float get_sfx_volume()
Get the current SFX volume multiplier.
void set_listener(const Vec2< float > &position, const Vec2< float > &velocity={ 0.0F, 0.0F })
Set where the listener is, usually the player or camera centre.
void set_music_volume(float volume)
Set the music volume multiplier. Same as the Music bus volume.
void set_master_volume(float volume)
Set the master volume multiplier (affects all audio).
void set_bus_volume(Bus bus, float volume)
Set a bus volume.
void stop_music(float fade_out_s=0.0F)
Stop the currently playing music.
SoundHandle play(const asw::Sample &sample, float volume=1.0F, float pan=0.0F, bool loop=false)
Play a sample.
bool _init()
Initialize the sound module. Called automatically by asw::core::init().
SpatialMode
How positional sounds are placed on the speakers.
@ Stereo
Left/right panning. Best for headphones and side on games.
Vec2< float > get_listener()
Get the listener position.
void duck(Bus bus, float gain, float hold_s, float fade_s=0.2F)
Turn a bus down for a while, for example music under dialogue or everything under a big explosion....
@ Linear
Straight line from full volume at min_distance to silent at max_distance.
@ Inverse
Natural sounding, loud close up and a long quiet tail.
@ InverseSquare
Like Inverse but drops faster.
float get_bus_volume(Bus bus)
Get a bus volume.
uint32_t _get_session()
Get the number of times the sound module has been shut down.
void set_spatial_mode(SpatialMode mode)
Choose how positional sounds are placed on the speakers.
SoundHandle play_at(const asw::Sample &sample, float x, float volume=1.0F)
Play a sample panned by where it happens on screen. Sounds pan towards the side they are on and fade ...
SoundHandle play_positional(const asw::Sample &sample, const Vec2< float > &position, const PlayOptions &options={})
Play a sample at a point in the world. It is panned, faded and pitch shifted relative to the listener...
void _update()
Update fades, ducking and positional sounds. Called automatically by asw::core::update().
std::shared_ptr< MIX_Audio > Sample
Alias for a shared pointer to an MIX_Audio.
std::shared_ptr< MIX_Audio > Music
Alias for a shared pointer to an MIX_Audio.
Random module for the ASW library.
Sound module for the ASW library.
std::atomic< float > cutoff_hz
std::array< float, MAX_FILTER_CHANNELS > state
asw::Vec2< float > position
asw::Vec2< float > velocity
asw::sound::Attenuation attenuation
struct anonymous_namespace{sound.cpp}::Voice::Sent sent
Distance settings for positional sounds, in world units (pixels).
float min_distance
Full volume at this distance or closer.
float max_distance
Silent at this distance or further.
Rolloff rolloff
Shape of the fall off between the two.
static SoundHandle make(int voice, uint32_t generation)
Options for playing a sound.
float fade_in_s
Fade in time in seconds.
float pitch
Playback speed and pitch. 1.0 is normal, 2.0 is an octave up.
float volume_variation
Random volume change per play, as a fraction.
float pan
Panning (-1.0 left - 1.0 right). Ignored for positional sounds.
float volume
Playback volume (0.0 - 1.0).
Attenuation attenuation
Distance fall off, used by positional sounds.