From 7cab6e9df950b76f2bd26d07cb9eb967a90e73ff Mon Sep 17 00:00:00 2001 From: daniel martinez lozano <161240228+Zum0DePapaya@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:42:33 +0200 Subject: [PATCH 1/2] Add module-aaudio-source, an AAudio capture source Without a real capture source, the only recording device PulseAudio offers is AAudioSink.monitor. Wine enumerates monitors as Windows recording devices, so games pick the monitor up as a microphone - in a voice chat that means the game rebroadcasts its own output, i.e. the whole lobby, back into the lobby. This adds the input counterpart to module-aaudio-sink. Structure deliberately mirrors the sink: an AAudio callback hands buffers to the PulseAudio I/O thread over an asyncmsgq, the same recreate / try-start / try-stop message pattern handles stream loss, and the state machine lives in set_state_in_io_thread. Defaults to AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION for the hardware echo canceller and noise suppressor, which matters because the game's output is usually coming from the same device's speaker. That call is API 28, guarded by get_android_sdk_version() the same way the sink guards setUsage. Two deliberate differences from the sink: - The AAudio stream is stopped whenever the source is not RUNNING, not only when suspended. A sink holding an idle output stream costs nothing, but a source holding an idle input stream keeps the microphone open, which lights the Android privacy indicator for the whole session and runs into the background-capture restrictions on Android 14+. - pa__done stops the stream and joins the I/O thread before returning, so unloading the module actually releases the microphone. If RECORD_AUDIO has not been granted, openStream fails and pa__init returns -1. The daemon runs with --fail=false, so that degrades to today's behaviour rather than taking audio down. Verified: compiles clean with -Wall against the PA 13.0 tree, links at API 26, 16 KB page aligned, and every undefined pa_* symbol resolves against the shipped libpulsecore/libpulsecommon/libpulse. Not yet exercised on a device - loading it needs the app-side change in the GameNative repo to add the load-module line. Co-Authored-By: Claude Opus 5 --- pulseaudio-module/CMakeLists.txt | 14 + pulseaudio-module/build.sh | 4 +- pulseaudio-module/create-asset.sh | 1 + pulseaudio-module/module-aaudio-source.c | 582 +++++++++++++++++++++++ 4 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 pulseaudio-module/module-aaudio-source.c diff --git a/pulseaudio-module/CMakeLists.txt b/pulseaudio-module/CMakeLists.txt index f7a1ff4..5beebbd 100644 --- a/pulseaudio-module/CMakeLists.txt +++ b/pulseaudio-module/CMakeLists.txt @@ -26,3 +26,17 @@ target_link_libraries(module-aaudio-sink pulsecommon-13.0 pulse aaudio) + +add_library(module-aaudio-source SHARED + module-aaudio-source.c) + +set_target_properties(module-aaudio-source PROPERTIES + OUTPUT_NAME "module-aaudio-source" + PREFIX "" + SUFFIX ".so") + +target_link_libraries(module-aaudio-source + pulsecore-13.0 + pulsecommon-13.0 + pulse + aaudio) diff --git a/pulseaudio-module/build.sh b/pulseaudio-module/build.sh index a76c69c..c947843 100755 --- a/pulseaudio-module/build.sh +++ b/pulseaudio-module/build.sh @@ -15,9 +15,11 @@ export CFLAGS="-I../pulseaudio/build-arm64 -I../pulseaudio/src -I../root-arm64/i export LDFLAGS="-L../root-arm64/lib/pulseaudio -L../root-arm64/lib -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" $CC -O2 -shared $CFLAGS $LDFLAGS -lpulsecore-13.0 -lpulsecommon-13.0 -lpulse -laaudio -llog -o build64/module-aaudio-sink.so module-aaudio-sink.c +$CC -O2 -shared $CFLAGS $LDFLAGS -lpulsecore-13.0 -lpulsecommon-13.0 -lpulse -laaudio -llog -o build64/module-aaudio-source.so module-aaudio-source.c export CC="$TOOLCHAIN/armv7a-linux-androideabi26-clang" export CFLAGS="-I../pulseaudio/build-armhf -I../pulseaudio/src -I../root-armhf/include" export LDFLAGS="-L../root-armhf/lib/pulseaudio -L../root-armhf/lib -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" -$CC -O2 -shared $CFLAGS $LDFLAGS -lpulsecore-13.0 -lpulsecommon-13.0 -lpulse -laaudio -llog -o build/module-aaudio-sink.so module-aaudio-sink.c \ No newline at end of file +$CC -O2 -shared $CFLAGS $LDFLAGS -lpulsecore-13.0 -lpulsecommon-13.0 -lpulse -laaudio -llog -o build/module-aaudio-sink.so module-aaudio-sink.c +$CC -O2 -shared $CFLAGS $LDFLAGS -lpulsecore-13.0 -lpulsecommon-13.0 -lpulse -laaudio -llog -o build/module-aaudio-source.so module-aaudio-source.c \ No newline at end of file diff --git a/pulseaudio-module/create-asset.sh b/pulseaudio-module/create-asset.sh index 2d3ea18..c48641b 100755 --- a/pulseaudio-module/create-asset.sh +++ b/pulseaudio-module/create-asset.sh @@ -7,6 +7,7 @@ cp -a ../output/aarch64-linux-gnu/modules/libprotocol-native.so pulseaudio/modul cp -a ../output/aarch64-linux-gnu/modules/module-native-protocol-unix.so pulseaudio/modules cp -a build64/module-aaudio-sink.so pulseaudio/modules +cp -a build64/module-aaudio-source.so pulseaudio/modules tar -I 'zstd --ultra -22' -cf pulseaudio-gamenative.tzst -C pulseaudio . diff --git a/pulseaudio-module/module-aaudio-source.c b/pulseaudio-module/module-aaudio-source.c new file mode 100644 index 0000000..1dac7a9 --- /dev/null +++ b/pulseaudio-module/module-aaudio-source.c @@ -0,0 +1,582 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2008 Lennart Poettering + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, + or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see . +***/ + +/* AAudio capture source, the input counterpart to module-aaudio-sink. + * + * Without a real source, the only capture device PulseAudio offers is + * AAudioSink.monitor. Wine enumerates monitors as Windows recording devices, so + * games pick it up as a microphone and end up transmitting their own output - + * in a voice chat that means rebroadcasting the whole lobby back into it. + * + * Structure deliberately mirrors module-aaudio-sink.c: an AAudio callback hands + * buffers to the PulseAudio I/O thread over an asyncmsgq, the same recreate / + * try-start / try-stop message pattern handles stream loss, and the state machine + * lives in set_state_in_io_thread. + * + * Two deliberate differences from the sink: + * + * - The AAudio stream is stopped whenever the source is not RUNNING, not just + * when it is suspended. A sink holding an idle output stream costs nothing, + * but a source holding an idle input stream keeps the microphone open, which + * lights the Android privacy indicator for the whole session and runs into + * the background-capture restrictions on Android 14+. + * + * - pa__done stops the stream and joins the I/O thread before returning, so + * unloading the module actually releases the microphone. + */ + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#undef __INTRODUCED_IN +#define __INTRODUCED_IN(api_level) +#include +#include + +#define LOG_TAG "GN-PulseAudioSource" +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) + +PA_MODULE_AUTHOR("Tom Yan, BrunoSX, Joshua Tam"); +PA_MODULE_DESCRIPTION("Winlator AAudio source"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(false); +PA_MODULE_USAGE( + "source_name= " + "source_properties= " + "rate= " + "channels= " + "performance_mode= " + "input_preset= " + "set_default=" +); + +#define DEFAULT_SOURCE_NAME "AAudioSource" + +/* AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION - gives us the hardware echo canceller + * and noise suppressor, which matters because the game's own output is usually + * coming out of the same device's speaker. */ +#define DEFAULT_INPUT_PRESET 7 + +enum { + SOURCE_MESSAGE_CAPTURE = PA_SOURCE_MESSAGE_MAX, + SOURCE_MESSAGE_TRY_START, + SOURCE_MESSAGE_TRY_STOP, + SOURCE_MESSAGE_RECREATE, +}; + +struct userdata { + pa_core *core; + pa_module *module; + pa_source *source; + + pa_thread *thread; + pa_thread_mq thread_mq; + pa_rtpoll *rtpoll; + pa_rtpoll_item *rtpoll_item; + pa_asyncmsgq *aaudio_msgq; + + size_t frame_size; + + AAudioStreamBuilder *builder; + AAudioStream *stream; + pa_sample_spec ss; + + int performance_mode; + int input_preset; + bool set_default; + + int32_t frames_per_burst; + int32_t device_sample_rate; + int32_t device_channels; + int32_t last_device_id; + + bool stream_started; +}; + +static const char* const valid_modargs[] = { + "source_name", + "source_properties", + "rate", + "channels", + "performance_mode", + "input_preset", + "set_default", + NULL +}; + +static int get_android_sdk_version(void) { + char sdk_version_str[PROP_VALUE_MAX]; + if (__system_property_get("ro.build.version.sdk", sdk_version_str) > 0) { + return atoi(sdk_version_str); + } + return 0; +} + +static void schedule_start(struct userdata *u) { + pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->source), SOURCE_MESSAGE_TRY_START, NULL, 0, NULL, NULL); +} + +static void schedule_stop(struct userdata *u) { + pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->source), SOURCE_MESSAGE_TRY_STOP, NULL, 0, NULL, NULL); +} + +static void schedule_recreate(struct userdata *u) { + pa_asyncmsgq_post(u->thread_mq.inq, PA_MSGOBJECT(u->source), SOURCE_MESSAGE_RECREATE, NULL, 0, NULL, NULL); +} + +/* Runs on the AAudio callback thread. Hand the buffer to the I/O thread and block + * until it has been posted, so audioData stays valid for the whole call. */ +static aaudio_data_callback_result_t aaudio_data_callback(AAudioStream *stream, void *userdata, void *audioData, int32_t numFrames) { + struct userdata *u = userdata; + return pa_asyncmsgq_send(u->aaudio_msgq, PA_MSGOBJECT(u->source), SOURCE_MESSAGE_CAPTURE, audioData, numFrames, NULL); +} + +static void aaudio_error_callback(AAudioStream *stream, void *userdata, aaudio_result_t error) { + struct userdata *u = userdata; + + if (error == AAUDIO_ERROR_DISCONNECTED || + error == AAUDIO_ERROR_INVALID_STATE || + error == AAUDIO_ERROR_INVALID_HANDLE || + error == AAUDIO_ERROR_TIMEOUT) { + LOGW("AAudio stream error (%d), attempting to reconnect...", error); + schedule_recreate(u); + } else { + LOGW("AAudio error callback: %d", error); + } +} + +static pa_usec_t get_aaudio_latency(struct userdata *u) { + if (u->stream != NULL && u->ss.rate > 0) { + int32_t buffer_size = AAudioStream_getBufferSizeInFrames(u->stream); + if (buffer_size > 0) { + return PA_USEC_PER_SEC * (int64_t) (buffer_size + u->frames_per_burst) / u->ss.rate; + } + } + + return 20 * PA_USEC_PER_MSEC; +} + +static void update_pa_latency(struct userdata *u) { + if (u->source) { + pa_usec_t latency = get_aaudio_latency(u); + if (pa_thread_mq_get()) { + pa_source_set_fixed_latency_within_thread(u->source, latency); + } else { + pa_source_set_fixed_latency(u->source, latency); + } + } +} + +static int pa_create_aaudio_stream(struct userdata *u) { + aaudio_result_t res; + + res = AAudio_createStreamBuilder(&u->builder); + if (res != AAUDIO_OK) { + LOGW("AAudio_createStreamBuilder() failed."); + return -1; + } + + AAudioStreamBuilder_setDirection(u->builder, AAUDIO_DIRECTION_INPUT); + AAudioStreamBuilder_setPerformanceMode(u->builder, u->performance_mode); + AAudioStreamBuilder_setDataCallback(u->builder, aaudio_data_callback, u); + AAudioStreamBuilder_setErrorCallback(u->builder, aaudio_error_callback, u); + AAudioStreamBuilder_setFormat(u->builder, u->ss.format == PA_SAMPLE_FLOAT32LE ? AAUDIO_FORMAT_PCM_FLOAT : AAUDIO_FORMAT_PCM_I16); + + /* Let AAudio pick the device's native capture rate and channel count and adapt + * the source to whatever we are given, rather than forcing a resample here. */ + AAudioStreamBuilder_setSampleRate(u->builder, AAUDIO_UNSPECIFIED); + AAudioStreamBuilder_setChannelCount(u->builder, AAUDIO_UNSPECIFIED); + + /* setInputPreset is API 28. Guarded the same way the sink guards setUsage. */ + if (get_android_sdk_version() >= 28) { + AAudioStreamBuilder_setInputPreset(u->builder, u->input_preset); + } + + res = AAudioStreamBuilder_openStream(u->builder, &u->stream); + if (res != AAUDIO_OK) { + /* The most likely cause here is a missing or revoked RECORD_AUDIO grant. */ + LOGW("AAudioStreamBuilder_openStream() failed: %d (%s)", res, AAudio_convertResultToText(res)); + AAudioStreamBuilder_delete(u->builder); + u->builder = NULL; + return -1; + } + + AAudioStreamBuilder_delete(u->builder); + u->builder = NULL; + + u->device_sample_rate = AAudioStream_getSampleRate(u->stream); + u->device_channels = AAudioStream_getChannelCount(u->stream); + aaudio_format_t actual_format = AAudioStream_getFormat(u->stream); + + LOGW("AAudio input stream opened: %d Hz, %d channels, format %d, device %d", + u->device_sample_rate, u->device_channels, actual_format, + AAudioStream_getDeviceId(u->stream)); + + u->ss.rate = u->device_sample_rate; + u->ss.channels = u->device_channels; + + if (actual_format == AAUDIO_FORMAT_PCM_FLOAT) { + u->ss.format = PA_SAMPLE_FLOAT32LE; + } else if (actual_format == AAUDIO_FORMAT_PCM_I16) { + u->ss.format = PA_SAMPLE_S16LE; + } + + u->frames_per_burst = AAudioStream_getFramesPerBurst(u->stream); + u->last_device_id = AAudioStream_getDeviceId(u->stream); + u->frame_size = pa_frame_size(&u->ss); + u->stream_started = false; + + update_pa_latency(u); + + return 0; +} + +static void try_start_stream(struct userdata *u) { + aaudio_result_t res; + + if (!u->stream || u->stream_started) return; + + res = AAudioStream_requestStart(u->stream); + if (res == AAUDIO_OK) { + u->stream_started = true; + LOGW("AAudioStream_requestStart() succeeded"); + } else { + LOGW("AAudioStream_requestStart() failed: %d, scheduling recreate", res); + schedule_recreate(u); + } +} + +static void try_stop_stream(struct userdata *u) { + aaudio_result_t res; + + if (!u->stream || !u->stream_started) return; + + res = AAudioStream_requestStop(u->stream); + if (res != AAUDIO_OK) { + LOGW("AAudioStream_requestStop() failed: %d", res); + } else { + LOGW("AAudio input stream stopped, microphone released"); + } + u->stream_started = false; +} + +static void recreate_aaudio_stream(struct userdata *u) { + if (u->stream) { + AAudioStream_requestStop(u->stream); + AAudioStream_close(u->stream); + u->stream = NULL; + u->stream_started = false; + } + + if (pa_create_aaudio_stream(u) < 0) { + LOGW("Failed to create AAudio input stream"); + /* Deliberately not rescheduling: if the microphone permission is denied, + * retrying forever would spin. The next state transition will retry. */ + } else { + LOGW("AAudio input stream created, attempting to start"); + schedule_start(u); + } +} + +static int source_process_capture(struct userdata *u, void *audioData, int64_t numFrames) { + pa_memchunk chunk; + + if (!PA_SOURCE_IS_LINKED(u->source->thread_info.state)) return AAUDIO_CALLBACK_RESULT_STOP; + + /* Drop the buffer rather than posting it while suspended or not running. */ + if (u->source->thread_info.state != PA_SOURCE_RUNNING) return AAUDIO_CALLBACK_RESULT_CONTINUE; + + if (numFrames <= 0) return AAUDIO_CALLBACK_RESULT_CONTINUE; + + /* audioData belongs to AAudio and is only valid for this call. pa_source_post + * copies into the source outputs' buffers before returning, and the callback + * thread is blocked in pa_asyncmsgq_send until we get here, so wrapping it + * read-only without copying is safe. */ + chunk.memblock = pa_memblock_new_fixed(u->core->mempool, audioData, u->frame_size * numFrames, true); + chunk.index = 0; + chunk.length = u->frame_size * numFrames; + + pa_source_post(u->source, &chunk); + + pa_memblock_unref_fixed(chunk.memblock); + + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} + +static int source_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *memchunk) { + struct userdata *u = PA_SOURCE(o)->userdata; + + if (code == SOURCE_MESSAGE_CAPTURE) return source_process_capture(u, data, offset); + + if (code == SOURCE_MESSAGE_TRY_START) { + LOGW("AAudio try start requested"); + if (u->source->thread_info.state == PA_SOURCE_RUNNING) { + try_start_stream(u); + } else { + LOGW("Source no longer running, canceling try"); + } + return 0; + } + + if (code == SOURCE_MESSAGE_TRY_STOP) { + LOGW("AAudio try stop requested"); + try_stop_stream(u); + return 0; + } + + if (code == SOURCE_MESSAGE_RECREATE) { + LOGW("AAudio recreate requested"); + if (PA_SOURCE_IS_LINKED(u->source->thread_info.state)) { + recreate_aaudio_stream(u); + } else { + LOGW("Source no longer linked, canceling recreate"); + } + return 0; + } + + return pa_source_process_msg(o, code, data, offset, memchunk); +} + +static int source_set_state_io_thread(pa_source *s, pa_source_state_t state, pa_suspend_cause_t suspend_cause) { + struct userdata *u = s->userdata; + + LOGW("AAudio source state transition: current=%d, target=%d, suspend_cause=%d", + s->thread_info.state, state, suspend_cause); + + /* Hold the microphone open only while something is actually recording. */ + if (state == PA_SOURCE_RUNNING) { + if (!u->stream) { + LOGW("No AAudio stream, recreating before start"); + recreate_aaudio_stream(u); + } else { + aaudio_stream_state_t stream_state = AAudioStream_getState(u->stream); + if (stream_state == AAUDIO_STREAM_STATE_STARTED || + stream_state == AAUDIO_STREAM_STATE_STARTING) { + u->stream_started = true; + } else if (stream_state == AAUDIO_STREAM_STATE_DISCONNECTED) { + LOGW("AAudio stream disconnected, recreating"); + schedule_recreate(u); + } else { + schedule_start(u); + } + } + } else { + /* IDLE, SUSPENDED or UNLINKED - all mean nobody is recording right now. */ + if (u->stream_started) { + schedule_stop(u); + } + } + + return 0; +} + +static void thread_func(void *userdata) { + struct userdata *u = userdata; + pa_thread_mq_install(&u->thread_mq); + + for (;;) { + int res = pa_rtpoll_run(u->rtpoll); + if (res < 0) { + goto error; + } else if (res == 0) { + break; + } + } + + return; + +error: + pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL, NULL); + pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN); +} + +void pa__done(pa_module *m) { + struct userdata *u; + + if (!(u = m->userdata)) return; + + if (u->source) pa_source_unlink(u->source); + + /* Join the I/O thread before touching the stream, so nothing is mid-post. */ + if (u->thread) { + pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL); + pa_thread_free(u->thread); + } + + pa_thread_mq_done(&u->thread_mq); + + if (u->source) pa_source_unref(u->source); + + /* Stop before close so the microphone is released promptly and the Android + * privacy indicator clears. */ + if (u->stream) { + AAudioStream_requestStop(u->stream); + AAudioStream_close(u->stream); + u->stream = NULL; + } + + if (u->builder) AAudioStreamBuilder_delete(u->builder); + if (u->rtpoll_item) pa_rtpoll_item_free(u->rtpoll_item); + if (u->aaudio_msgq) pa_asyncmsgq_unref(u->aaudio_msgq); + if (u->rtpoll) pa_rtpoll_free(u->rtpoll); + + pa_xfree(u); +} + +int pa__init(pa_module *m) { + struct userdata *u = NULL; + pa_modargs *ma = NULL; + pa_channel_map map; + pa_source_new_data data; + int performance_mode = 0; + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + LOGW("Failed to parse module arguments."); + goto error; + } + + m->userdata = u = pa_xnew0(struct userdata, 1); + + u->core = m->core; + u->module = m; + u->rtpoll = pa_rtpoll_new(); + + if (pa_thread_mq_init(&u->thread_mq, m->core->mainloop, u->rtpoll) < 0) { + LOGW("pa_thread_mq_init() failed."); + goto error; + } + + u->aaudio_msgq = pa_asyncmsgq_new(0); + if (!u->aaudio_msgq) { + LOGW("pa_asyncmsgq_new() failed."); + goto error; + } + + u->rtpoll_item = pa_rtpoll_item_new_asyncmsgq_read(u->rtpoll, PA_RTPOLL_EARLY-1, u->aaudio_msgq); + + u->ss = m->core->default_sample_spec; + map = m->core->default_channel_map; + + if (pa_modargs_get_sample_spec_and_channel_map(ma, &u->ss, &map, PA_CHANNEL_MAP_DEFAULT) < 0) { + LOGW("pa_modargs_get_sample_spec_and_channel_map() failed."); + goto error; + } + + /* Mono is what voice chat wants; overridden below by whatever AAudio opens. */ + u->ss.channels = 1; + u->ss.format = u->ss.format == PA_SAMPLE_FLOAT32LE || u->ss.format == PA_SAMPLE_FLOAT32BE ? PA_SAMPLE_FLOAT32LE : PA_SAMPLE_S16LE; + + u->performance_mode = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY; + u->input_preset = DEFAULT_INPUT_PRESET; + u->set_default = true; + + if (!pa_modargs_get_value_s32(ma, "performance_mode", &performance_mode)) { + switch (performance_mode) { + case 0: + u->performance_mode = AAUDIO_PERFORMANCE_MODE_NONE; + break; + case 1: + u->performance_mode = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY; + break; + case 2: + u->performance_mode = AAUDIO_PERFORMANCE_MODE_POWER_SAVING; + break; + } + } + + if (pa_modargs_get_value_s32(ma, "input_preset", &u->input_preset) < 0) { + LOGW("Failed to parse input_preset argument."); + goto error; + } + + if (pa_modargs_get_value_boolean(ma, "set_default", &u->set_default) < 0) { + LOGW("Failed to parse set_default argument."); + goto error; + } + + if (pa_create_aaudio_stream(u) < 0) goto error; + + /* The stream is opened here only to discover the device's real format; nothing + * is captured until a client connects and the source goes RUNNING. */ + pa_channel_map_init_extend(&map, u->ss.channels, PA_CHANNEL_MAP_DEFAULT); + + pa_source_new_data_init(&data); + data.driver = __FILE__; + data.module = m; + pa_source_new_data_set_name(&data, pa_modargs_get_value(ma, "source_name", DEFAULT_SOURCE_NAME)); + pa_source_new_data_set_sample_spec(&data, &u->ss); + pa_source_new_data_set_alternate_sample_rate(&data, u->ss.rate); + pa_source_new_data_set_channel_map(&data, &map); + + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, _("AAudio Input")); + pa_proplist_sets(data.proplist, PA_PROP_DEVICE_CLASS, "abstract"); + + if (pa_modargs_get_proplist(ma, "source_properties", data.proplist, PA_UPDATE_REPLACE) < 0) { + LOGW("pa_modargs_get_proplist() failed."); + pa_source_new_data_done(&data); + goto error; + } + + u->source = pa_source_new(m->core, &data, PA_SOURCE_HARDWARE); + pa_source_new_data_done(&data); + + if (!u->source) { + LOGW("Failed to create source object."); + goto error; + } + + u->source->parent.process_msg = source_process_msg; + u->source->set_state_in_io_thread = source_set_state_io_thread; + u->source->userdata = u; + + pa_source_set_asyncmsgq(u->source, u->thread_mq.inq); + pa_source_set_rtpoll(u->source, u->rtpoll); + update_pa_latency(u); + + if (!(u->thread = pa_thread_new("aaudio-source", thread_func, u))) { + LOGW("Failed to create thread."); + goto error; + } + + pa_source_put(u->source); + + /* Outrank AAudioSink.monitor, which PulseAudio creates for every sink and + * which Wine would otherwise hand to games as their microphone. */ + if (u->set_default) { + pa_core_set_configured_default_source(m->core, u->source->name); + } + + pa_modargs_free(ma); + return 0; + +error: + if (ma) pa_modargs_free(ma); + pa__done(m); + return -1; +} From 66344fb7fa5c73d20d52aa21b4f87176121d368f Mon Sep 17 00:00:00 2001 From: daniel martinez lozano <161240228+Zum0DePapaya@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:06:27 +0200 Subject: [PATCH 2/2] Declare setInputPreset weak so the module loads on API 26/27 AAudioStreamBuilder_setInputPreset is API 28 and this module is built at API 26. Blanking __INTRODUCED_IN lets it compile, but that also strips the annotation which would have made the reference weak, leaving a strong undefined symbol. The module is linked with -z now, so the dynamic linker resolves every undefined symbol at dlopen() time, before any module code runs -- a runtime API-level check cannot prevent the failure because it never gets to run. On API 26/27 libaaudio.so has no such symbol, so the module would fail to load outright. Declaring it weak lets the linker leave the address NULL instead of failing the load, so the NULL check is meaningful. This matches the approach taken for AAudioStreamBuilder_setUsage in the sink, so both modules now handle post-minSdk symbols the same way. Verified with llvm-readelf: the symbol is WEAK UND, no strong undefined AAudio symbol is missing from API 26, segments stay 16 KB aligned, and all 51 pa_* imports still resolve. Dropping dlfcn.h also removes the dlopen/dlsym imports. Incidentally fixes a signature mismatch: the old function pointer typedef returned aaudio_result_t, but the real entry point returns void. Co-Authored-By: Claude Opus 5 --- pulseaudio-module/module-aaudio-source.c | 34 +++++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/pulseaudio-module/module-aaudio-source.c b/pulseaudio-module/module-aaudio-source.c index 1dac7a9..7285eec 100644 --- a/pulseaudio-module/module-aaudio-source.c +++ b/pulseaudio-module/module-aaudio-source.c @@ -51,7 +51,6 @@ #include #include -#include #include #undef __INTRODUCED_IN #define __INTRODUCED_IN(api_level) @@ -130,12 +129,30 @@ static const char* const valid_modargs[] = { NULL }; -static int get_android_sdk_version(void) { - char sdk_version_str[PROP_VALUE_MAX]; - if (__system_property_get("ro.build.version.sdk", sdk_version_str) > 0) { - return atoi(sdk_version_str); +/* AAudioStreamBuilder_setInputPreset is API 28, and this module is built at API 26. + * Blanking __INTRODUCED_IN above lets it compile, but it also strips the annotation that + * would otherwise have made the reference weak, leaving a strong undefined symbol - and + * the module is linked with -z now, so the dynamic linker resolves every undefined symbol + * when the module is dlopen'd, before any of our code runs. On an API 26/27 device + * libaaudio.so does not export it, so the whole module would fail to load, and a runtime + * API-level check could not prevent that because it never gets the chance to run. + * Declaring it weak lets the linker leave the address NULL instead of failing the load, + * which is what makes the check below meaningful. Same approach as the sink. */ +extern __attribute__((weak)) void AAudioStreamBuilder_setInputPreset(AAudioStreamBuilder *builder, + aaudio_input_preset_t preset); + +static void try_set_input_preset(AAudioStreamBuilder *builder, int preset) { + static bool warned = false; + + if (AAudioStreamBuilder_setInputPreset) { + AAudioStreamBuilder_setInputPreset(builder, preset); + return; + } + + if (!warned) { + LOGW("AAudioStreamBuilder_setInputPreset unavailable (API < 28), using the default input preset"); + warned = true; } - return 0; } static void schedule_start(struct userdata *u) { @@ -213,10 +230,7 @@ static int pa_create_aaudio_stream(struct userdata *u) { AAudioStreamBuilder_setSampleRate(u->builder, AAUDIO_UNSPECIFIED); AAudioStreamBuilder_setChannelCount(u->builder, AAUDIO_UNSPECIFIED); - /* setInputPreset is API 28. Guarded the same way the sink guards setUsage. */ - if (get_android_sdk_version() >= 28) { - AAudioStreamBuilder_setInputPreset(u->builder, u->input_preset); - } + try_set_input_preset(u->builder, u->input_preset); res = AAudioStreamBuilder_openStream(u->builder, &u->stream); if (res != AAUDIO_OK) {