cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

pulse.c (10910B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "config.h"
     21
     22#include "pulse/pulse.h"
     23
     24#include <guacamole/audio.h>
     25#include <guacamole/mem.h>
     26#include <guacamole/client.h>
     27#include <guacamole/user.h>
     28#include <pulse/pulseaudio.h>
     29
     30/**
     31 * Returns whether the given buffer contains only silence (only null bytes).
     32 *
     33 * @param buffer
     34 *     The audio buffer to check.
     35 *
     36 * @param length
     37 *     The length of the buffer to check.
     38 *
     39 * @return
     40 *     Non-zero if the audio buffer contains silence, zero otherwise.
     41 */
     42static int guac_pa_is_silence(const void* buffer, size_t length) {
     43
     44    int i;
     45
     46    const unsigned char* current = (const unsigned char*) buffer;
     47
     48    /* For each byte in buffer */
     49    for (i = 0; i < length; i++) {
     50
     51        /* If current value non-zero, then not silence */
     52        if (*(current++))
     53            return 0;
     54
     55    }
     56
     57    /* Otherwise, the buffer contains 100% silence */
     58    return 1;
     59
     60}
     61
     62/**
     63 * Callback invoked by PulseAudio when PCM data is available for reading
     64 * from the given stream. The PCM data can be read using pa_stream_peek().
     65 *
     66 * @param stream
     67 *     The PulseAudio stream which has PCM data available.
     68 *
     69 * @param length
     70 *     The number of bytes of PCM data available on the given stream.
     71 *
     72 * @param data
     73 *     A pointer to the guac_pa_stream structure associated with the Guacamole
     74 *     stream receiving audio data from PulseAudio.
     75 */
     76static void __stream_read_callback(pa_stream* stream, size_t length,
     77        void* data) {
     78
     79    guac_pa_stream* guac_stream = (guac_pa_stream*) data;
     80    guac_audio_stream* audio = guac_stream->audio;
     81
     82    const void* buffer;
     83
     84    /* Read data */
     85    pa_stream_peek(stream, &buffer, &length);
     86
     87    /* Continuously write received PCM data */
     88    if (!guac_pa_is_silence(buffer, length))
     89        guac_audio_stream_write_pcm(audio, buffer, length);
     90
     91    /* Flush upon silence */
     92    else
     93        guac_audio_stream_flush(audio);
     94
     95    /* Advance buffer */
     96    pa_stream_drop(stream);
     97
     98}
     99
    100/**
    101 * Callback invoked by PulseAudio when the audio stream has changed state. The
    102 * new state can be retrieved using pa_stream_get_state().
    103 *
    104 * @param stream
    105 *     The PulseAudio stream which has changed state.
    106 *
    107 * @param data
    108 *     A pointer to the guac_pa_stream structure associated with the Guacamole
    109 *     stream receiving audio data from PulseAudio.
    110 */
    111static void __stream_state_callback(pa_stream* stream, void* data) {
    112
    113    guac_pa_stream* guac_stream = (guac_pa_stream*) data;
    114    guac_client* client = guac_stream->client;
    115
    116    switch (pa_stream_get_state(stream)) {
    117
    118        case PA_STREAM_UNCONNECTED:
    119            guac_client_log(client, GUAC_LOG_INFO,
    120                    "PulseAudio stream currently unconnected");
    121            break;
    122
    123        case PA_STREAM_CREATING:
    124            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio stream being created...");
    125            break;
    126
    127        case PA_STREAM_READY:
    128            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio stream now ready");
    129            break;
    130
    131        case PA_STREAM_FAILED:
    132            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio stream connection failed");
    133            break;
    134
    135        case PA_STREAM_TERMINATED:
    136            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio stream terminated");
    137            break;
    138
    139        default:
    140            guac_client_log(client, GUAC_LOG_INFO,
    141                    "Unknown PulseAudio stream state: 0x%x",
    142                    pa_stream_get_state(stream));
    143
    144    }
    145
    146}
    147
    148/**
    149 * Callback invoked by PulseAudio when the audio sink information has been
    150 * retrieved. The callback is invoked repeatedly, once for each available
    151 * sink, followed by one final invocation with the is_last flag set. The final
    152 * invocation (with is_last set) does not describe a sink; it serves as a
    153 * terminator only.
    154 *
    155 * @param context 
    156 *     The PulseAudio context which is providing the sink information.
    157 *
    158 * @param info
    159 *     Information describing an available audio sink.
    160 *
    161 * @param is_last
    162 *     Non-zero if this invocation is the final invocation of this callback for
    163 *     all currently-available sinks (and this invocation does not describe a
    164 *     sink), zero otherwise.
    165 *
    166 * @param data
    167 *     A pointer to the guac_pa_stream structure associated with the Guacamole
    168 *     stream receiving audio data from PulseAudio.
    169 */
    170static void __context_get_sink_info_callback(pa_context* context,
    171        const pa_sink_info* info, int is_last, void* data) {
    172
    173    guac_pa_stream* guac_stream = (guac_pa_stream*) data;
    174    guac_client* client = guac_stream->client;
    175
    176    pa_stream* stream;
    177    pa_sample_spec spec;
    178    pa_buffer_attr attr;
    179
    180    /* Stop if end of list reached */
    181    if (is_last)
    182        return;
    183
    184    guac_client_log(client, GUAC_LOG_INFO, "Starting streaming from \"%s\"",
    185            info->description);
    186
    187    /* Set format */
    188    spec.format   = PA_SAMPLE_S16LE;
    189    spec.rate     = GUAC_PULSE_AUDIO_RATE;
    190    spec.channels = GUAC_PULSE_AUDIO_CHANNELS;
    191
    192    attr.maxlength = -1;
    193    attr.fragsize  = GUAC_PULSE_AUDIO_FRAGMENT_SIZE;
    194
    195    /* Create stream */
    196    stream = pa_stream_new(context, "Guacamole Audio", &spec, NULL);
    197
    198    /* Set stream callbacks */
    199    pa_stream_set_state_callback(stream, __stream_state_callback, guac_stream);
    200    pa_stream_set_read_callback(stream, __stream_read_callback, guac_stream);
    201
    202    /* Start stream */
    203    pa_stream_connect_record(stream, info->monitor_source_name, &attr,
    204                PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND
    205              | PA_STREAM_ADJUST_LATENCY);
    206
    207}
    208
    209/**
    210 * Callback invoked by PulseAudio when server information has been retrieved.
    211 *
    212 * @param context 
    213 *     The PulseAudio context which is providing the sink information.
    214 *
    215 * @param info
    216 *     Information describing the PulseAudio server.
    217 *
    218 * @param data
    219 *     A pointer to the guac_pa_stream structure associated with the Guacamole
    220 *     stream receiving audio data from PulseAudio.
    221 */
    222static void __context_get_server_info_callback(pa_context* context,
    223        const pa_server_info* info, void* data) {
    224
    225    guac_pa_stream* guac_stream = (guac_pa_stream*) data;
    226    guac_client* client = guac_stream->client;
    227
    228    /* If no default sink, cannot continue */
    229    if (info->default_sink_name == NULL) {
    230        guac_client_log(client, GUAC_LOG_ERROR, "No default sink. Cannot stream audio.");
    231        return;
    232    }
    233
    234    guac_client_log(client, GUAC_LOG_INFO, "Will use default sink: \"%s\"",
    235            info->default_sink_name);
    236
    237    /* Wait for default sink information */
    238    pa_operation_unref(
    239            pa_context_get_sink_info_by_name(context,
    240                info->default_sink_name, __context_get_sink_info_callback,
    241                guac_stream));
    242
    243}
    244
    245/**
    246 * Callback invoked by PulseAudio when the overall audio context has changed
    247 * state. The new state can be retrieved using pa_context_get_state().
    248 *
    249 * @param context 
    250 *     The PulseAudio context which has changed state.
    251 *
    252 * @param data
    253 *     A pointer to the guac_pa_stream structure associated with the Guacamole
    254 *     stream receiving audio data from PulseAudio.
    255 */
    256static void __context_state_callback(pa_context* context, void* data) {
    257
    258    guac_pa_stream* guac_stream = (guac_pa_stream*) data;
    259    guac_client* client = guac_stream->client;
    260
    261    switch (pa_context_get_state(context)) {
    262
    263        case PA_CONTEXT_UNCONNECTED:
    264            guac_client_log(client, GUAC_LOG_INFO,
    265                    "PulseAudio reports it is unconnected");
    266            break;
    267
    268        case PA_CONTEXT_CONNECTING:
    269            guac_client_log(client, GUAC_LOG_INFO, "Connecting to PulseAudio...");
    270            break;
    271
    272        case PA_CONTEXT_AUTHORIZING:
    273            guac_client_log(client, GUAC_LOG_INFO,
    274                    "Authorizing PulseAudio connection...");
    275            break;
    276
    277        case PA_CONTEXT_SETTING_NAME:
    278            guac_client_log(client, GUAC_LOG_INFO, "Sending client name...");
    279            break;
    280
    281        case PA_CONTEXT_READY:
    282            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio now ready");
    283            pa_operation_unref(pa_context_get_server_info(context,
    284                        __context_get_server_info_callback, guac_stream));
    285            break;
    286
    287        case PA_CONTEXT_FAILED:
    288            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio connection failed");
    289            break;
    290
    291        case PA_CONTEXT_TERMINATED:
    292            guac_client_log(client, GUAC_LOG_INFO, "PulseAudio connection terminated");
    293            break;
    294
    295        default:
    296            guac_client_log(client, GUAC_LOG_INFO,
    297                    "Unknown PulseAudio context state: 0x%x",
    298                    pa_context_get_state(context));
    299
    300    }
    301
    302}
    303
    304guac_pa_stream* guac_pa_stream_alloc(guac_client* client,
    305        const char* server_name) {
    306
    307    guac_audio_stream* audio = guac_audio_stream_alloc(client, NULL,
    308            GUAC_PULSE_AUDIO_RATE, GUAC_PULSE_AUDIO_CHANNELS,
    309            GUAC_PULSE_AUDIO_BPS);
    310
    311    /* Abort if audio stream cannot be created */
    312    if (audio == NULL)
    313        return NULL;
    314
    315    /* Init main loop */
    316    guac_pa_stream* stream = guac_mem_alloc(sizeof(guac_pa_stream));
    317    stream->client = client;
    318    stream->audio = audio;
    319    stream->pa_mainloop = pa_threaded_mainloop_new();
    320
    321    /* Create context */
    322    pa_context* context = pa_context_new(
    323            pa_threaded_mainloop_get_api(stream->pa_mainloop),
    324            "Guacamole Audio");
    325
    326    /* Set up context */
    327    pa_context_set_state_callback(context, __context_state_callback, stream);
    328    pa_context_connect(context, server_name, PA_CONTEXT_NOAUTOSPAWN, NULL);
    329
    330    /* Start loop */
    331    pa_threaded_mainloop_start(stream->pa_mainloop);
    332
    333    return stream;
    334
    335}
    336
    337void guac_pa_stream_add_user(guac_pa_stream* stream, guac_user* user) {
    338    guac_audio_stream_add_user(stream->audio, user);
    339}
    340
    341void guac_pa_stream_free(guac_pa_stream* stream) {
    342
    343    /* Stop loop */
    344    pa_threaded_mainloop_stop(stream->pa_mainloop);
    345
    346    /* Free underlying audio stream */
    347    guac_audio_stream_free(stream->audio);
    348
    349    /* Stream now ended */
    350    guac_client_log(stream->client, GUAC_LOG_INFO, "Audio stream finished");
    351    guac_mem_free(stream);
    352
    353}
    354