/* Copyright (c) 2024 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ // https://freedesktop.org/software/pulseaudio/doxygen/simple.html #include "nativeaudio/AudioOut.h" #include "nativeaudio/AudioOut_private.h" #include "pulse/pulseaudio.h" #include static bool initialized; static pa_threaded_mainloop * paMainLoop; static pa_context * paContext; static pa_stream * paStream; static void * transportBuffer; #define CHUNK_SIZE 1024 #define BUFFER_SIZE 65536 #define TRANSPORT_BUFFER_SIZE 65536 #define SINK_COUNT_MAX 10 static void contextStateCallback(pa_context * context, void * userdata) { switch (pa_context_get_state(context)) { case PA_CONTEXT_READY: case PA_CONTEXT_FAILED: case PA_CONTEXT_TERMINATED: pa_threaded_mainloop_signal(paMainLoop, 0); break; case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; } } static void sinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, void * userdata) { if (info != NULL) { bool * outSinkFound = userdata; g_AudioOut_hostFormat.sampleRate = info->sample_spec.rate; *outSinkFound = true; } pa_threaded_mainloop_signal(paMainLoop, 0); } static void streamStateCallback(pa_stream * stream, void * userdata) { switch (pa_stream_get_state(stream)) { case PA_STREAM_READY: case PA_STREAM_FAILED: case PA_STREAM_TERMINATED: pa_threaded_mainloop_signal(paMainLoop, 0); break; case PA_STREAM_UNCONNECTED: case PA_STREAM_CREATING: break; } } static void streamWriteCallback(pa_stream * stream, size_t nbytes, void * userdata) { void * outputBuffer; pa_stream_begin_write(stream, &outputBuffer, &nbytes); if (g_AudioOut_outputActive) { AudioOut_transferToOutput(outputBuffer, g_AudioOut_outputCallback, g_AudioOut_outputContext, nbytes / g_AudioOut_hostFormat.bytesPerSample / g_AudioOut_hostFormat.channelCount, transportBuffer, TRANSPORT_BUFFER_SIZE, g_AudioOut_hostFormat, g_AudioOut_transportFormat, &g_AudioOut_resampleState); } else { memset(outputBuffer, 0, nbytes); } pa_stream_write(stream, outputBuffer, nbytes, NULL, 0, PA_SEEK_RELATIVE); } static void waitForPAOperation(pa_operation * operation) { while (pa_operation_get_state(operation) != PA_OPERATION_DONE) { pa_threaded_mainloop_wait(paMainLoop); } } void AudioOut_init(const char * processName) { if (initialized) { return; } transportBuffer = calloc(TRANSPORT_BUFFER_SIZE, g_AudioOut_hostFormat.bytesPerSample); paMainLoop = pa_threaded_mainloop_new(); pa_threaded_mainloop_lock(paMainLoop); paContext = pa_context_new(pa_threaded_mainloop_get_api(paMainLoop), processName); pa_context_set_state_callback(paContext, contextStateCallback, NULL); pa_context_connect(paContext, NULL, PA_CONTEXT_NOFLAGS, NULL); pa_threaded_mainloop_start(paMainLoop); pa_threaded_mainloop_wait(paMainLoop); pa_sample_spec sampleSpec; switch (g_AudioOut_hostFormat.bytesPerSample) { case 1: sampleSpec.format = PA_SAMPLE_U8; break; case 2: sampleSpec.format = PA_SAMPLE_S16LE; break; case 3: sampleSpec.format = PA_SAMPLE_S24LE; break; case 4: sampleSpec.format = PA_SAMPLE_FLOAT32LE; break; default: #ifdef DEBUG fprintf(stderr, "AudioOut_init: Unknown bytesPerSample: %u\n", g_AudioOut_hostFormat.bytesPerSample); #endif abort(); } bool sinkFound = false; for (unsigned int sinkIndex = 0; sinkIndex < SINK_COUNT_MAX && !sinkFound; sinkIndex++) { waitForPAOperation(pa_context_get_sink_info_by_index(paContext, sinkIndex, sinkInfoCallback, &sinkFound)); } sampleSpec.rate = g_AudioOut_hostFormat.sampleRate; sampleSpec.channels = g_AudioOut_hostFormat.channelCount; g_AudioOut_transportFormat = g_AudioOut_hostFormat; paStream = pa_stream_new(paContext, processName, &sampleSpec, NULL); pa_stream_set_state_callback(paStream, streamStateCallback, NULL); pa_stream_set_write_callback(paStream, streamWriteCallback, NULL); pa_buffer_attr bufferAttributes; //bufferAttributes.maxlength = BUFFER_SIZE * g_AudioOut_hostFormat.bytesPerSample; bufferAttributes.maxlength = -1; bufferAttributes.tlength = CHUNK_SIZE * g_AudioOut_hostFormat.bytesPerSample; //bufferAttributes.prebuf = CHUNK_SIZE * g_AudioOut_hostFormat.bytesPerSample; bufferAttributes.prebuf = -1; //bufferAttributes.minreq = CHUNK_SIZE * g_AudioOut_hostFormat.bytesPerSample; bufferAttributes.minreq = -1; bufferAttributes.fragsize = 0; pa_stream_connect_playback(paStream, NULL, &bufferAttributes, PA_STREAM_START_CORKED | PA_STREAM_START_UNMUTED, NULL, NULL); pa_threaded_mainloop_wait(paMainLoop); pa_threaded_mainloop_unlock(paMainLoop); initialized = true; } void AudioOut_shutdown(void) { if (initialized) { pa_threaded_mainloop_stop(paMainLoop); pa_context_disconnect(paContext); pa_context_unref(paContext); pa_stream_unref(paStream); pa_threaded_mainloop_free(paMainLoop); free(transportBuffer); transportBuffer = NULL; initialized = false; } } void AudioOut_setHostFormat(AudioOut_sampleFormat format) { if (format.channelCount == g_AudioOut_hostFormat.channelCount && format.bytesPerSample == g_AudioOut_hostFormat.bytesPerSample && format.sampleRate == g_AudioOut_hostFormat.sampleRate) { return; } if (initialized) { // Changing host format after init is not supported in current pulseaudio nativeaudio implementation } else { g_AudioOut_hostFormat = format; } } static void operationSuccessCallback(pa_stream * stream, int success, void * userdata) { pa_threaded_mainloop_signal(paMainLoop, 0); } void AudioOut_startOutput(AudioOutCallback callback, void * context) { if (!g_AudioOut_outputActive) { pa_threaded_mainloop_lock(paMainLoop); g_AudioOut_outputCallback = callback; g_AudioOut_outputContext = context; g_AudioOut_outputActive = true; waitForPAOperation(pa_stream_cork(paStream, 0, operationSuccessCallback, NULL)); pa_threaded_mainloop_unlock(paMainLoop); } } void AudioOut_stopOutput(void) { if (g_AudioOut_outputActive) { pa_threaded_mainloop_lock(paMainLoop); g_AudioOut_outputActive = false; waitForPAOperation(pa_stream_cork(paStream, 1, operationSuccessCallback, NULL)); pa_threaded_mainloop_unlock(paMainLoop); } }