From 3e8bf3ba91b8cdaca216996ca2266329782ff425 Mon Sep 17 00:00:00 2001 From: Julian Todd Date: Fri, 28 Nov 2025 17:28:12 +0000 Subject: [PATCH] Access microphone buffer from AudioServer and prevent microphone double starts --- doc/classes/AudioServer.xml | 29 ++++++++ .../pulseaudio/audio_driver_pulseaudio.cpp | 6 ++ drivers/wasapi/audio_driver_wasapi.cpp | 8 ++- platform/android/audio_driver_opensl.cpp | 28 ++++---- servers/audio/audio_server.cpp | 72 +++++++++++++++++++ servers/audio/audio_server.h | 7 ++ servers/audio/audio_stream.cpp | 9 +-- 7 files changed, 136 insertions(+), 23 deletions(-) diff --git a/doc/classes/AudioServer.xml b/doc/classes/AudioServer.xml index e33eeb73f11..16437fd7d86 100644 --- a/doc/classes/AudioServer.xml +++ b/doc/classes/AudioServer.xml @@ -124,6 +124,12 @@ Returns the name of the current audio driver. The default usually depends on the operating system, but may be overridden via the [code]--audio-driver[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url]. [code]--headless[/code] also automatically sets the audio driver to [code]Dummy[/code]. See also [member ProjectSettings.audio/driver/driver]. + + + + Returns the absolute size of the microphone input buffer. This is set to a multiple of the audio latency and can be used to estimate the minimum rate at which the frames need to be fetched. + + @@ -131,6 +137,21 @@ [b]Note:[/b] [member ProjectSettings.audio/driver/enable_input] must be [code]true[/code] for audio input to work. See also that setting's description for caveats related to permissions and operating system privacy settings. + + + + + Returns a [PackedVector2Array] containing exactly [param frames] audio samples from the internal microphone buffer if available, otherwise returns an empty [PackedVector2Array]. + The buffer is filled at the rate of [method get_input_mix_rate] frames per second when [method set_input_device_active] has successfully been set to [code]true[/code]. + The samples are signed floating-point PCM values between [code]-1[/code] and [code]1[/code]. + + + + + + Returns the number of frames available to read using [method get_input_frames]. + + @@ -330,6 +351,14 @@ [b]Note:[/b] This is enabled by default in the editor, as it is used by editor plugins for the audio stream previews. + + + + + If [param active] is [code]true[/code], starts the microphone input stream specified by [member input_device] or returns an error if it failed. + If [param active] is [code]false[/code], stops the input stream if it is running. + + diff --git a/drivers/pulseaudio/audio_driver_pulseaudio.cpp b/drivers/pulseaudio/audio_driver_pulseaudio.cpp index f5fde816a16..8bc88495d3c 100644 --- a/drivers/pulseaudio/audio_driver_pulseaudio.cpp +++ b/drivers/pulseaudio/audio_driver_pulseaudio.cpp @@ -555,6 +555,8 @@ void AudioDriverPulseAudio::thread_func(void *p_udata) { } // User selected a new input device, finish the current one so we'll init the new input device + // (If `AudioServer.set_input_device()` did not set the value when the microphone was running, + // this section with its problematic error handling could be deleted.) if (ad->input_device_name != ad->new_input_device) { ad->input_device_name = ad->new_input_device; ad->finish_input_device(); @@ -691,6 +693,10 @@ void AudioDriverPulseAudio::finish() { } Error AudioDriverPulseAudio::init_input_device() { + if (pa_rec_str) { + return ERR_ALREADY_IN_USE; + } + // If there is a specified input device, check that it is really present if (input_device_name != "Default") { PackedStringArray list = get_input_device_list(); diff --git a/drivers/wasapi/audio_driver_wasapi.cpp b/drivers/wasapi/audio_driver_wasapi.cpp index bbccc6f28fd..3cdfd4ca545 100644 --- a/drivers/wasapi/audio_driver_wasapi.cpp +++ b/drivers/wasapi/audio_driver_wasapi.cpp @@ -542,6 +542,10 @@ Error AudioDriverWASAPI::init_output_device(bool p_reinit) { } Error AudioDriverWASAPI::init_input_device(bool p_reinit) { + if (audio_input.active.is_set()) { + return ERR_ALREADY_IN_USE; + } + Error err = audio_device_init(&audio_input, true, p_reinit); if (err != OK) { // We've tried to init the device, but have failed. Time to clean up. @@ -1023,11 +1027,9 @@ Error AudioDriverWASAPI::input_stop() { if (audio_input.active.is_set()) { audio_input.audio_client->Stop(); audio_input.active.clear(); - - return OK; } - return FAILED; + return OK; } PackedStringArray AudioDriverWASAPI::get_input_device_list() { diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 8455a933979..9dfb60ac084 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -271,7 +271,11 @@ Error AudioDriverOpenSL::input_start() { } if (OS::get_singleton()->request_permission("RECORD_AUDIO")) { - return init_input_device(); + Error err = init_input_device(); + if (err != OK) { + input_stop(); + } + return err; } WARN_PRINT("Unable to start audio capture - No RECORD_AUDIO permission"); @@ -279,20 +283,18 @@ Error AudioDriverOpenSL::input_start() { } Error AudioDriverOpenSL::input_stop() { - if (!recordItf || !recordBufferQueueItf) { - return ERR_CANT_OPEN; + if (recordItf) { + (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED); + recordItf = nullptr; } - SLuint32 state; - SLresult res = (*recordItf)->GetRecordState(recordItf, &state); - ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN); - - if (state != SL_RECORDSTATE_STOPPED) { - res = (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED); - ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN); - - res = (*recordBufferQueueItf)->Clear(recordBufferQueueItf); - ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN); + if (recordBufferQueueItf) { + (*recordBufferQueueItf)->Clear(recordBufferQueueItf); + recordBufferQueueItf = nullptr; + } + if (recorder) { + (*recorder)->Destroy(recorder); + recorder = nullptr; } return OK; diff --git a/servers/audio/audio_server.cpp b/servers/audio/audio_server.cpp index 07d38316496..fa6715dec9e 100644 --- a/servers/audio/audio_server.cpp +++ b/servers/audio/audio_server.cpp @@ -109,6 +109,9 @@ void AudioDriver::input_buffer_write(int32_t sample) { input_size++; } } else { + // This protection was added in GH-26505 due to a "possible crash". + // This cannot have happened unless two non-locked threads entered function simultaneously, which was possible when multiple calls to + // `AudioDriver::input_start()` did not raise an error condition. WARN_PRINT("input_buffer_write: Invalid input_position=" + itos(input_position) + " input_buffer.size()=" + itos(input_buffer.size())); } } @@ -1842,6 +1845,71 @@ void AudioServer::set_input_device(const String &p_name) { AudioDriver::get_singleton()->set_input_device(p_name); } +Error AudioServer::set_input_device_active(bool p_is_active) { + if (input_device_active == p_is_active) { + return OK; + } + if (p_is_active) { + if (!GLOBAL_GET("audio/driver/enable_input")) { + WARN_PRINT("You must enable the project setting \"audio/driver/enable_input\" to use audio capture."); + return FAILED; + } + + input_buffer_ofs = 0; + input_device_active = true; + return AudioDriver::get_singleton()->input_start(); + } else { + input_device_active = false; + return AudioDriver::get_singleton()->input_stop(); + } +} + +int AudioServer::get_input_frames_available() { + AudioDriver *ad = AudioDriver::get_singleton(); + ad->lock(); + int64_t input_position = ad->get_input_position(); + if (input_position < input_buffer_ofs) { + input_position += ad->get_input_buffer().size(); + } + ad->unlock(); + return (int)((input_position - input_buffer_ofs) / 2); // Buffer is stereo. +} + +int AudioServer::get_input_buffer_length_frames() { + AudioDriver *ad = AudioDriver::get_singleton(); + ad->lock(); + int buffsize = ad->get_input_buffer().size(); + ad->unlock(); + return buffsize / 2; +} + +PackedVector2Array AudioServer::get_input_frames(int p_frames) { + PackedVector2Array ret; + AudioDriver *ad = AudioDriver::get_singleton(); + ad->lock(); + int input_position = ad->get_input_position(); + Vector buf = ad->get_input_buffer(); + if (input_position < input_buffer_ofs) { + input_position += buf.size(); + } + if ((input_buffer_ofs + p_frames * 2 <= input_position) && (p_frames >= 0)) { + ret.resize(p_frames); + for (int i = 0; i < p_frames; i++) { + float l = (buf[input_buffer_ofs++] >> 16) / 32768.f; + if (input_buffer_ofs >= buf.size()) { + input_buffer_ofs = 0; + } + float r = (buf[input_buffer_ofs++] >> 16) / 32768.f; + if (input_buffer_ofs >= buf.size()) { + input_buffer_ofs = 0; + } + ret.write[i] = Vector2(l, r); + } + } + ad->unlock(); + return ret; +} + void AudioServer::set_enable_tagging_used_audio_streams(bool p_enable) { tag_used_audio_streams = p_enable; } @@ -2016,6 +2084,10 @@ void AudioServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_input_device_list"), &AudioServer::get_input_device_list); ClassDB::bind_method(D_METHOD("get_input_device"), &AudioServer::get_input_device); ClassDB::bind_method(D_METHOD("set_input_device", "name"), &AudioServer::set_input_device); + ClassDB::bind_method(D_METHOD("set_input_device_active", "active"), &AudioServer::set_input_device_active); + ClassDB::bind_method(D_METHOD("get_input_frames_available"), &AudioServer::get_input_frames_available); + ClassDB::bind_method(D_METHOD("get_input_buffer_length_frames"), &AudioServer::get_input_buffer_length_frames); + ClassDB::bind_method(D_METHOD("get_input_frames", "frames"), &AudioServer::get_input_frames); ClassDB::bind_method(D_METHOD("set_bus_layout", "bus_layout"), &AudioServer::set_bus_layout); ClassDB::bind_method(D_METHOD("generate_bus_layout"), &AudioServer::generate_bus_layout); diff --git a/servers/audio/audio_server.h b/servers/audio/audio_server.h index e429c83616b..a7a4decf835 100644 --- a/servers/audio/audio_server.h +++ b/servers/audio/audio_server.h @@ -232,6 +232,9 @@ private: bool debug_mute = false; #endif // DEBUG_ENABLED + bool input_device_active = false; + int input_buffer_ofs = 0; + struct Bus { StringName name; bool solo = false; @@ -492,6 +495,10 @@ public: PackedStringArray get_input_device_list(); String get_input_device(); void set_input_device(const String &p_name); + Error set_input_device_active(bool p_is_active); + int get_input_frames_available(); + int get_input_buffer_length_frames(); + PackedVector2Array get_input_frames(int p_frames); void set_enable_tagging_used_audio_streams(bool p_enable); diff --git a/servers/audio/audio_stream.cpp b/servers/audio/audio_stream.cpp index 5cfd35916cd..452eb104b95 100644 --- a/servers/audio/audio_stream.cpp +++ b/servers/audio/audio_stream.cpp @@ -442,14 +442,9 @@ void AudioStreamPlaybackMicrophone::start(double p_from_pos) { return; } - if (!GLOBAL_GET_CACHED(bool, "audio/driver/enable_input")) { - WARN_PRINT("You must enable the project setting \"audio/driver/enable_input\" to use audio capture."); - return; - } - input_ofs = 0; - if (AudioDriver::get_singleton()->input_start() == OK) { + if (AudioServer::get_singleton()->set_input_device_active(true) == OK) { active = true; begin_resample(); } @@ -457,7 +452,7 @@ void AudioStreamPlaybackMicrophone::start(double p_from_pos) { void AudioStreamPlaybackMicrophone::stop() { if (active) { - AudioDriver::get_singleton()->input_stop(); + AudioServer::get_singleton()->set_input_device_active(false); active = false; } }