diff options
author | Ellen Poe <ellenhp@google.com> | 2021-08-27 10:28:23 -0700 |
---|---|---|
committer | Ellen Poe <ellenhp@google.com> | 2021-08-27 10:41:57 -0700 |
commit | 3598d300cb43797a4f18b34d921875d060ce7de7 (patch) | |
tree | 080fee412849b0f18905c6100dab9b242b32226c /servers/audio_server.cpp | |
parent | 53843ba872e3e76c05e8bf9a86d1876914e1ba89 (diff) |
Do all audio mixing in the AudioServer
Diffstat (limited to 'servers/audio_server.cpp')
-rw-r--r-- | servers/audio_server.cpp | 492 |
1 files changed, 458 insertions, 34 deletions
diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index 4c54188cb2..81735d522f 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -32,13 +32,19 @@ #include "core/config/project_settings.h" #include "core/debugger/engine_debugger.h" +#include "core/error/error_macros.h" #include "core/io/file_access.h" #include "core/io/resource_loader.h" +#include "core/math/audio_frame.h" #include "core/os/os.h" +#include "core/string/string_name.h" +#include "core/templates/pair.h" #include "scene/resources/audio_stream_sample.h" #include "servers/audio/audio_driver_dummy.h" #include "servers/audio/effects/audio_effect_compressor.h" +#include <cstring> + #ifdef TOOLS_ENABLED #define MARK_EDITED set_edited(true); #else @@ -234,6 +240,7 @@ AudioDriver *AudioDriverManager::get_driver(int p_driver) { ////////////////////////////////////////////// void AudioServer::_driver_process(int p_frames, int32_t *p_buffer) { + mix_count++; int todo = p_frames; #ifdef DEBUG_ENABLED @@ -331,10 +338,156 @@ void AudioServer::_mix_step() { bus->soloed = false; } } + for (CallbackItem *ci : mix_callback_list) { + ci->callback(ci->userdata); + } + + for (AudioStreamPlaybackListNode *playback : playback_list) { + // Paused streams are no-ops. Don't even mix audio from the stream playback. + if (playback->state.load() == AudioStreamPlaybackListNode::PAUSED) { + continue; + } + + bool fading_out = playback->state.load() == AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION || playback->state.load() == AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE; + + AudioFrame *buf = mix_buffer.ptrw(); + + // Copy the lookeahead buffer into the mix buffer. + for (int i = 0; i < LOOKAHEAD_BUFFER_SIZE; i++) { + buf[i] = playback->lookahead[i]; + } + + // Mix the audio stream + unsigned int mixed_frames = playback->stream_playback->mix(&buf[LOOKAHEAD_BUFFER_SIZE], playback->pitch_scale.get(), buffer_size); + + if (mixed_frames != buffer_size) { + // We know we have at least the size of our lookahead buffer for fade-out purposes. + + float fadeout_base = 0.87; + float fadeout_coefficient = 1; + static_assert(LOOKAHEAD_BUFFER_SIZE == 32, "Update fadeout_base and comment here if you change LOOKAHEAD_BUFFER_SIZE."); + // 0.87 ^ 32 = 0.0116. There might still be a pop but it'll be way better than if we didn't do this. + for (unsigned int idx = mixed_frames; idx < buffer_size; idx++) { + fadeout_coefficient *= fadeout_base; + buf[idx] *= fadeout_coefficient; + } + AudioStreamPlaybackListNode::PlaybackState new_state; + new_state = AudioStreamPlaybackListNode::AWAITING_DELETION; + playback->state.store(new_state); + } else { + // Move the last little bit of what we just mixed into our lookahead buffer. + for (int i = 0; i < LOOKAHEAD_BUFFER_SIZE; i++) { + playback->lookahead[i] = buf[buffer_size + i]; + } + } + + ERR_FAIL_COND(playback->bus_details.load() == nullptr); + // By putting null into the bus details pointers, we're taking ownership of their memory for the duration of this mix. + AudioStreamPlaybackBusDetails *bus_details = nullptr; + { + std::atomic<AudioStreamPlaybackBusDetails *> bus_details_atomic = nullptr; + bus_details = playback->bus_details.exchange(bus_details_atomic); + } + ERR_FAIL_COND(bus_details == nullptr); + AudioStreamPlaybackBusDetails *prev_bus_details = playback->prev_bus_details; + + // Mix to any active buses. + for (int idx = 0; idx < MAX_BUSES_PER_PLAYBACK; idx++) { + if (!bus_details->bus_active[idx]) { + continue; + } + int bus_idx = thread_find_bus_index(bus_details->bus[idx]); + + int prev_bus_idx = -1; + for (int search_idx = 0; search_idx < MAX_BUSES_PER_PLAYBACK; search_idx++) { + if (!prev_bus_details->bus_active[search_idx]) { + continue; + } + if (prev_bus_details->bus[search_idx].hash() == bus_details->bus[idx].hash()) { + prev_bus_idx = search_idx; + } + } + + for (int channel_idx = 0; channel_idx < channel_count; channel_idx++) { + AudioFrame *channel_buf = thread_get_channel_mix_buffer(bus_idx, channel_idx); + if (fading_out) { + bus_details->volume[idx][channel_idx] = AudioFrame(0, 0); + } + AudioFrame channel_vol = bus_details->volume[idx][channel_idx]; + + AudioFrame prev_channel_vol = AudioFrame(0, 0); + if (prev_bus_idx != -1) { + prev_channel_vol = prev_bus_details->volume[prev_bus_idx][channel_idx]; + } + _mix_step_for_channel(channel_buf, buf, prev_channel_vol, channel_vol, playback->attenuation_filter_cutoff_hz.get(), playback->highshelf_gain.get(), &playback->filter_process[channel_idx * 2], &playback->filter_process[channel_idx * 2 + 1]); + } + } + + // Now go through and fade-out any buses that were being played to previously that we missed by going through current data. + for (int idx = 0; idx < MAX_BUSES_PER_PLAYBACK; idx++) { + if (!prev_bus_details->bus_active[idx]) { + continue; + } + int bus_idx = thread_find_bus_index(prev_bus_details->bus[idx]); + + int current_bus_idx = -1; + for (int search_idx = 0; search_idx < MAX_BUSES_PER_PLAYBACK; search_idx++) { + if (bus_details->bus[search_idx] == prev_bus_details->bus[idx]) { + current_bus_idx = search_idx; + } + } + if (current_bus_idx != -1) { + // If we found a corresponding bus in the current bus assignments, we've already mixed to this bus. + continue; + } + + for (int channel_idx = 0; channel_idx < channel_count; channel_idx++) { + AudioFrame *channel_buf = thread_get_channel_mix_buffer(bus_idx, channel_idx); + AudioFrame prev_channel_vol = prev_bus_details->volume[idx][channel_idx]; + // Fade out to silence + _mix_step_for_channel(channel_buf, buf, prev_channel_vol, AudioFrame(0, 0), playback->attenuation_filter_cutoff_hz.get(), playback->highshelf_gain.get(), &playback->filter_process[channel_idx * 2], &playback->filter_process[channel_idx * 2 + 1]); + } + } + + // Copy the bus details we mixed with to the previous bus details to maintain volume ramps. + std::copy(std::begin(bus_details->bus_active), std::end(bus_details->bus_active), std::begin(prev_bus_details->bus_active)); + std::copy(std::begin(bus_details->bus), std::end(bus_details->bus), std::begin(prev_bus_details->bus)); + for (int bus_idx = 0; bus_idx < MAX_BUSES_PER_PLAYBACK; bus_idx++) { + std::copy(std::begin(bus_details->volume[bus_idx]), std::end(bus_details->volume[bus_idx]), std::begin(prev_bus_details->volume[bus_idx])); + } + + AudioStreamPlaybackBusDetails *bus_details_expected = nullptr; + // Only put the bus details pointer back if it hasn't been updated already. + if (!playback->bus_details.compare_exchange_strong(/* expected= */ bus_details_expected, /* new= */ bus_details)) { + // If it *has* been updated already, queue the old one for deletion. + bus_details_graveyard.insert(bus_details); + } - //make callbacks for mixing the audio - for (Set<CallbackItem>::Element *E = callbacks.front(); E; E = E->next()) { - E->get().callback(E->get().userdata); + switch (playback->state.load()) { + case AudioStreamPlaybackListNode::AWAITING_DELETION: + case AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION: + playback_list.erase(playback, [](AudioStreamPlaybackListNode *p) { + if (p->prev_bus_details) + delete p->prev_bus_details; + if (p->bus_details) + delete p->bus_details; + p->stream_playback.unref(); + delete p; + }); + break; + case AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE: { + // Pause the stream. + AudioStreamPlaybackListNode::PlaybackState old_state, new_state; + do { + old_state = playback->state.load(); + new_state = AudioStreamPlaybackListNode::PAUSED; + } while (!playback->state.compare_exchange_strong(/* expected= */ old_state, new_state)); + } break; + case AudioStreamPlaybackListNode::PLAYING: + case AudioStreamPlaybackListNode::PAUSED: + // No-op! + break; + } } for (int i = buses.size() - 1; i >= 0; i--) { @@ -464,6 +617,53 @@ void AudioServer::_mix_step() { to_mix = buffer_size; } +void AudioServer::_mix_step_for_channel(AudioFrame *p_out_buf, AudioFrame *p_source_buf, AudioFrame p_vol_start, AudioFrame p_vol_final, float p_attenuation_filter_cutoff_hz, float p_highshelf_gain, AudioFilterSW::Processor *p_processor_l, AudioFilterSW::Processor *p_processor_r) { + if (p_highshelf_gain != 0) { + AudioFilterSW filter; + filter.set_mode(AudioFilterSW::HIGHSHELF); + filter.set_sampling_rate(AudioServer::get_singleton()->get_mix_rate()); + filter.set_cutoff(p_attenuation_filter_cutoff_hz); + filter.set_resonance(1); + filter.set_stages(1); + filter.set_gain(p_highshelf_gain); + + ERR_FAIL_COND(p_processor_l == nullptr); + ERR_FAIL_COND(p_processor_r == nullptr); + + bool is_just_started = p_vol_start.l == 0 && p_vol_start.r == 0; + p_processor_l->set_filter(&filter, /* clear_history= */ is_just_started); + p_processor_l->update_coeffs(buffer_size); + p_processor_r->set_filter(&filter, /* clear_history= */ is_just_started); + p_processor_r->update_coeffs(buffer_size); + + for (unsigned int frame_idx = 0; frame_idx < buffer_size; frame_idx++) { + // Make this buffer size invariant if buffer_size ever becomes a project setting. + float lerp_param = (float)frame_idx / buffer_size; + AudioFrame vol = p_vol_final * lerp_param + (1 - lerp_param) * p_vol_start; + AudioFrame mixed = vol * p_source_buf[frame_idx]; + p_processor_l->process_one_interp(mixed.l); + p_processor_r->process_one_interp(mixed.r); + p_out_buf[frame_idx] += mixed; + } + + } else { + for (unsigned int frame_idx = 0; frame_idx < buffer_size; frame_idx++) { + // Make this buffer size invariant if buffer_size ever becomes a project setting. + float lerp_param = (float)frame_idx / buffer_size; + p_out_buf[frame_idx] += (p_vol_final * lerp_param + (1 - lerp_param) * p_vol_start) * p_source_buf[frame_idx]; + } + } +} + +AudioServer::AudioStreamPlaybackListNode *AudioServer::_find_playback_list_node(Ref<AudioStreamPlayback> p_playback) { + for (AudioStreamPlaybackListNode *playback_list_node : playback_list) { + if (playback_list_node->stream_playback == p_playback) { + return playback_list_node; + } + } + return nullptr; +} + bool AudioServer::thread_has_channel_mix_buffer(int p_bus, int p_buffer) const { if (p_bus < 0 || p_bus >= buses.size()) { return false; @@ -923,9 +1123,216 @@ float AudioServer::get_playback_speed_scale() const { return playback_speed_scale; } +void AudioServer::start_playback_stream(Ref<AudioStreamPlayback> p_playback, StringName p_bus, Vector<AudioFrame> p_volume_db_vector, float p_start_time) { + ERR_FAIL_COND(p_playback.is_null()); + + Map<StringName, Vector<AudioFrame>> map; + map[p_bus] = p_volume_db_vector; + + start_playback_stream(p_playback, map, p_start_time); +} + +void AudioServer::start_playback_stream(Ref<AudioStreamPlayback> p_playback, Map<StringName, Vector<AudioFrame>> p_bus_volumes, float p_start_time) { + ERR_FAIL_COND(p_playback.is_null()); + + AudioStreamPlaybackListNode *playback_node = new AudioStreamPlaybackListNode(); + playback_node->stream_playback = p_playback; + playback_node->stream_playback->start(p_start_time); + + AudioStreamPlaybackBusDetails *new_bus_details = new AudioStreamPlaybackBusDetails(); + int idx = 0; + for (KeyValue<StringName, Vector<AudioFrame>> pair : p_bus_volumes) { + ERR_FAIL_COND(pair.value.size() < channel_count); + ERR_FAIL_COND(pair.value.size() != MAX_CHANNELS_PER_BUS); + + new_bus_details->bus_active[idx] = true; + new_bus_details->bus[idx] = pair.key; + for (int channel_idx = 0; channel_idx < MAX_CHANNELS_PER_BUS; channel_idx++) { + new_bus_details->volume[idx][channel_idx] = pair.value[channel_idx]; + } + } + playback_node->bus_details = new_bus_details; + playback_node->prev_bus_details = new AudioStreamPlaybackBusDetails(); + + playback_node->setseek.set(-1); + playback_node->pitch_scale.set(1); + playback_node->highshelf_gain.set(0); + playback_node->attenuation_filter_cutoff_hz.set(0); + + memset(playback_node->prev_bus_details->volume, 0, sizeof(playback_node->prev_bus_details->volume)); + + for (AudioFrame &frame : playback_node->lookahead) { + frame = AudioFrame(0, 0); + } + + playback_node->state.store(AudioStreamPlaybackListNode::PLAYING); + + playback_list.insert(playback_node); +} + +void AudioServer::stop_playback_stream(Ref<AudioStreamPlayback> p_playback) { + ERR_FAIL_COND(p_playback.is_null()); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + + AudioStreamPlaybackListNode::PlaybackState new_state, old_state; + do { + old_state = playback_node->state.load(); + new_state = AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION; + + } while (!playback_node->state.compare_exchange_strong(old_state, new_state)); +} + +void AudioServer::set_playback_bus_exclusive(Ref<AudioStreamPlayback> p_playback, StringName p_bus, Vector<AudioFrame> p_volumes) { + ERR_FAIL_COND(p_volumes.size() != MAX_CHANNELS_PER_BUS); + + Map<StringName, Vector<AudioFrame>> map; + map[p_bus] = p_volumes; + + set_playback_bus_volumes_linear(p_playback, map); +} + +void AudioServer::set_playback_bus_volumes_linear(Ref<AudioStreamPlayback> p_playback, Map<StringName, Vector<AudioFrame>> p_bus_volumes) { + ERR_FAIL_COND(p_bus_volumes.size() > MAX_BUSES_PER_PLAYBACK); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + AudioStreamPlaybackBusDetails *old_bus_details, *new_bus_details = new AudioStreamPlaybackBusDetails(); + + int idx = 0; + for (KeyValue<StringName, Vector<AudioFrame>> pair : p_bus_volumes) { + ERR_FAIL_COND(pair.value.size() < channel_count); + ERR_FAIL_COND(pair.value.size() != MAX_CHANNELS_PER_BUS); + + new_bus_details->bus_active[idx] = true; + new_bus_details->bus[idx] = pair.key; + for (int channel_idx = 0; channel_idx < MAX_CHANNELS_PER_BUS; channel_idx++) { + new_bus_details->volume[idx][channel_idx] = pair.value[channel_idx]; + } + } + + do { + old_bus_details = playback_node->bus_details.load(); + } while (!playback_node->bus_details.compare_exchange_strong(old_bus_details, new_bus_details)); + + bus_details_graveyard.insert(old_bus_details); +} + +void AudioServer::set_playback_all_bus_volumes_linear(Ref<AudioStreamPlayback> p_playback, Vector<AudioFrame> p_volumes) { + ERR_FAIL_COND(p_playback.is_null()); + ERR_FAIL_COND(p_volumes.size() != MAX_CHANNELS_PER_BUS); + + Map<StringName, Vector<AudioFrame>> map; + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + for (int bus_idx = 0; bus_idx < MAX_BUSES_PER_PLAYBACK; bus_idx++) { + if (playback_node->bus_details.load()->bus_active[bus_idx]) { + map[playback_node->bus_details.load()->bus[bus_idx]] = p_volumes; + } + } + + set_playback_bus_volumes_linear(p_playback, map); +} + +void AudioServer::set_playback_pitch_scale(Ref<AudioStreamPlayback> p_playback, float p_pitch_scale) { + ERR_FAIL_COND(p_playback.is_null()); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + + playback_node->pitch_scale.set(p_pitch_scale); +} + +void AudioServer::set_playback_paused(Ref<AudioStreamPlayback> p_playback, bool p_paused) { + ERR_FAIL_COND(p_playback.is_null()); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + if (!p_paused && playback_node->state == AudioStreamPlaybackListNode::PLAYING) { + return; // No-op. + } + if (p_paused && (playback_node->state == AudioStreamPlaybackListNode::PAUSED || playback_node->state == AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE)) { + return; // No-op. + } + + AudioStreamPlaybackListNode::PlaybackState new_state, old_state; + do { + old_state = playback_node->state.load(); + new_state = p_paused ? AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE : AudioStreamPlaybackListNode::PLAYING; + } while (!playback_node->state.compare_exchange_strong(old_state, new_state)); +} + +void AudioServer::set_playback_highshelf_params(Ref<AudioStreamPlayback> p_playback, float p_gain, float p_attenuation_cutoff_hz) { + ERR_FAIL_COND(p_playback.is_null()); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return; + } + + playback_node->attenuation_filter_cutoff_hz.set(p_attenuation_cutoff_hz); + playback_node->highshelf_gain.set(p_gain); +} + +bool AudioServer::is_playback_active(Ref<AudioStreamPlayback> p_playback) { + ERR_FAIL_COND_V(p_playback.is_null(), false); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return false; + } + + return playback_node->state.load() == AudioStreamPlaybackListNode::PLAYING; +} + +float AudioServer::get_playback_position(Ref<AudioStreamPlayback> p_playback) { + ERR_FAIL_COND_V(p_playback.is_null(), 0); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return 0; + } + + return playback_node->stream_playback->get_playback_position(); +} + +bool AudioServer::is_playback_paused(Ref<AudioStreamPlayback> p_playback) { + ERR_FAIL_COND_V(p_playback.is_null(), false); + + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (!playback_node) { + return false; + } + + return playback_node->state.load() == AudioStreamPlaybackListNode::PAUSED || playback_node->state.load() == AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE; +} + +uint64_t AudioServer::get_mix_count() const { + return mix_count; +} + +void AudioServer::notify_listener_changed() { + for (CallbackItem *ci : listener_changed_callback_list) { + ci->callback(ci->userdata); + } +} + void AudioServer::init_channels_and_buffers() { channel_count = get_channel_count(); temp_buffer.resize(channel_count); + mix_buffer.resize(buffer_size + LOOKAHEAD_BUFFER_SIZE); for (int i = 0; i < temp_buffer.size(); i++) { temp_buffer.write[i].resize(buffer_size); @@ -943,7 +1350,7 @@ void AudioServer::init() { channel_disable_threshold_db = GLOBAL_DEF_RST("audio/buses/channel_disable_threshold_db", -60.0); channel_disable_frames = float(GLOBAL_DEF_RST("audio/buses/channel_disable_time", 2.0)) * get_mix_rate(); ProjectSettings::get_singleton()->set_custom_property_info("audio/buses/channel_disable_time", PropertyInfo(Variant::FLOAT, "audio/buses/channel_disable_time", PROPERTY_HINT_RANGE, "0,5,0.01,or_greater")); - buffer_size = 1024; //hardcoded for now + buffer_size = 512; //hardcoded for now init_channels_and_buffers(); @@ -1030,9 +1437,17 @@ void AudioServer::update() { prof_time = 0; #endif - for (Set<CallbackItem>::Element *E = update_callbacks.front(); E; E = E->next()) { - E->get().callback(E->get().userdata); + for (CallbackItem *ci : update_callback_list) { + ci->callback(ci->userdata); } + mix_callback_list.maybe_cleanup(); + update_callback_list.maybe_cleanup(); + listener_changed_callback_list.maybe_cleanup(); + playback_list.maybe_cleanup(); + for (AudioStreamPlaybackBusDetails *bus_details : bus_details_graveyard) { + bus_details_graveyard.erase(bus_details, [](AudioStreamPlaybackBusDetails *d) { delete d; }); + } + bus_details_graveyard.maybe_cleanup(); } void AudioServer::load_default_bus_layout() { @@ -1098,40 +1513,49 @@ double AudioServer::get_time_since_last_mix() const { AudioServer *AudioServer::singleton = nullptr; -void AudioServer::add_callback(AudioCallback p_callback, void *p_userdata) { - lock(); - CallbackItem ci; - ci.callback = p_callback; - ci.userdata = p_userdata; - callbacks.insert(ci); - unlock(); +void AudioServer::add_update_callback(AudioCallback p_callback, void *p_userdata) { + CallbackItem *ci = new CallbackItem(); + ci->callback = p_callback; + ci->userdata = p_userdata; + update_callback_list.insert(ci); } -void AudioServer::remove_callback(AudioCallback p_callback, void *p_userdata) { - lock(); - CallbackItem ci; - ci.callback = p_callback; - ci.userdata = p_userdata; - callbacks.erase(ci); - unlock(); +void AudioServer::remove_update_callback(AudioCallback p_callback, void *p_userdata) { + for (CallbackItem *ci : update_callback_list) { + if (ci->callback == p_callback && ci->userdata == p_userdata) { + update_callback_list.erase(ci, [](CallbackItem *c) { delete c; }); + } + } } -void AudioServer::add_update_callback(AudioCallback p_callback, void *p_userdata) { - lock(); - CallbackItem ci; - ci.callback = p_callback; - ci.userdata = p_userdata; - update_callbacks.insert(ci); - unlock(); +void AudioServer::add_mix_callback(AudioCallback p_callback, void *p_userdata) { + CallbackItem *ci = new CallbackItem(); + ci->callback = p_callback; + ci->userdata = p_userdata; + mix_callback_list.insert(ci); } -void AudioServer::remove_update_callback(AudioCallback p_callback, void *p_userdata) { - lock(); - CallbackItem ci; - ci.callback = p_callback; - ci.userdata = p_userdata; - update_callbacks.erase(ci); - unlock(); +void AudioServer::remove_mix_callback(AudioCallback p_callback, void *p_userdata) { + for (CallbackItem *ci : mix_callback_list) { + if (ci->callback == p_callback && ci->userdata == p_userdata) { + mix_callback_list.erase(ci, [](CallbackItem *c) { delete c; }); + } + } +} + +void AudioServer::add_listener_changed_callback(AudioCallback p_callback, void *p_userdata) { + CallbackItem *ci = new CallbackItem(); + ci->callback = p_callback; + ci->userdata = p_userdata; + listener_changed_callback_list.insert(ci); +} + +void AudioServer::remove_listener_changed_callback(AudioCallback p_callback, void *p_userdata) { + for (CallbackItem *ci : listener_changed_callback_list) { + if (ci->callback == p_callback && ci->userdata == p_userdata) { + listener_changed_callback_list.erase(ci, [](CallbackItem *c) { delete c; }); + } + } } void AudioServer::set_bus_layout(const Ref<AudioBusLayout> &p_bus_layout) { |