pulse.c 19.1 KB
Newer Older
1
2
3
4
/*****************************************************************************
 * pulse.c : Pulseaudio output plugin for vlc
 *****************************************************************************
 * Copyright (C) 2008 the VideoLAN team
5
 * Copyright (C) 2009-2011 Rémi Denis-Courmont
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 *
 * Authors: Martin Hamrle <hamrle @ post . cz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

28
#include <vlc_common.h>
29
#include <vlc_plugin.h>
30
#include <vlc_aout.h>
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
31
#include <vlc_cpu.h>
32
33

#include <pulse/pulseaudio.h>
34
#include <vlc_pulse.h>
35
36
37
38

static int  Open        ( vlc_object_t * );
static void Close       ( vlc_object_t * );

39
vlc_module_begin ()
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
40
    set_shortname( "PulseAudio" )
41
    set_description( N_("Pulseaudio audio output") )
42
    set_capability( "audio output", 160 )
43
44
    set_category( CAT_AUDIO )
    set_subcategory( SUBCAT_AUDIO_AOUT )
45
    add_shortcut( "pulseaudio", "pa" )
46
47
    set_callbacks( Open, Close )
vlc_module_end ()
48

49
50
51
52
53
54
55
/* TODO:
 * - pause input on policy event
 * - resample to compensate for long term drift
 * - select music or video stream property correctly (?)
 * - set further appropriate stream properties
 * - update output devices list dynamically
 */
56

57
58
/* NOTE:
 * Be careful what you do when the PulseAudio mainloop is held, which is to say
59
 * within PulseAudio callbacks, or after vlc_pa_lock().
60
61
 * In particular, a VLC variable callback cannot be triggered nor deleted with
 * the PulseAudio mainloop lock held, if the callback acquires the lock. */
62

63
64
65
66
struct aout_sys_t
{
    pa_stream *stream; /**< PulseAudio playback stream object */
    pa_context *context; /**< PulseAudio connection context */
67
68
    pa_volume_t base_volume; /**< 0dB reference volume */
    pa_cvolume cvolume; /**< actual sink input volume */
69
70
};

71
/*** Sink ***/
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
static void sink_list_cb(pa_context *c, const pa_sink_info *i, int eol,
                         void *userdata)
{
    aout_instance_t *aout = userdata;
    vlc_value_t val, text;

    if (eol)
        return;
    (void) c;

    msg_Dbg(aout, "listing sink %s (%"PRIu32"): %s", i->name, i->index,
            i->description);
    val.i_int = i->index;
    text.psz_string = (char *)i->description;
    var_Change(aout, "audio-device", VLC_VAR_ADDCHOICE, &val, &text);
}

89
90
91
92
93
94
95
96
97
98
static void sink_info_cb(pa_context *c, const pa_sink_info *i, int eol,
                         void *userdata)
{
    aout_instance_t *aout = userdata;
    aout_sys_t *sys = aout->output.p_sys;

    if (eol)
        return;
    (void) c;

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
99
100
101
102
103
    /* PulseAudio flat volume NORM / 100% / 0dB corresponds to no software
     * amplification and maximum hardware amplification.
     * VLC maps DEFAULT / 100% to no gain at all (software/hardware).
     * Thus we need to use the sink base_volume as a multiplier,
     * if and only if flat volume is active for our current sink. */
104
105
106
107
108
109
110
    if (i->flags & PA_SINK_FLAT_VOLUME)
        sys->base_volume = i->base_volume;
    else
        sys->base_volume = PA_VOLUME_NORM;
    msg_Dbg(aout, "base volume: %f", pa_sw_volume_to_linear(sys->base_volume));
}

111
/*** Stream helpers ***/
112
113
114
115
116
117
static void stream_state_cb(pa_stream *s, void *userdata)
{
    switch (pa_stream_get_state(s)) {
        case PA_STREAM_READY:
        case PA_STREAM_FAILED:
        case PA_STREAM_TERMINATED:
118
            vlc_pa_signal(0);
119
120
121
        default:
            break;
    }
122
    (void) userdata;
123
124
125
126
}

static void stream_moved_cb(pa_stream *s, void *userdata)
{
127
128
129
130
    aout_instance_t *aout = userdata;
    aout_sys_t *sys = aout->output.p_sys;
    pa_operation *op;
    uint32_t idx = pa_stream_get_device_index(s);
131

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
132
133
134
135
    msg_Dbg(aout, "connected to sink %"PRIu32": %s", idx,
                  pa_stream_get_device_name(s));
    op = pa_context_get_sink_info_by_index(sys->context, idx,
                                           sink_info_cb, aout);
136
137
    if (likely(op != NULL))
        pa_operation_unref(op);
138
139
140
141

    /* Update the variable if someone else moved our stream */
    var_Change(aout, "audio-device", VLC_VAR_SETVALUE,
               &(vlc_value_t){ .i_int = idx }, NULL);
142
143
}

144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
static void stream_overflow_cb(pa_stream *s, void *userdata)
{
    aout_instance_t *aout = userdata;

    msg_Err(aout, "overflow");
    (void) s;
}

static void stream_started_cb(pa_stream *s, void *userdata)
{
    aout_instance_t *aout = userdata;

    msg_Dbg(aout, "started");
    (void) s;
}

static void stream_suspended_cb(pa_stream *s, void *userdata)
{
    aout_instance_t *aout = userdata;

    msg_Dbg(aout, "suspended");
    (void) s;
}

static void stream_underflow_cb(pa_stream *s, void *userdata)
{
    aout_instance_t *aout = userdata;
171
    pa_operation *op;
172

173
174
175
176
    msg_Warn(aout, "underflow");
    op = pa_stream_cork(s, 1, NULL, NULL);
    if (op != NULL)
        pa_operation_unref(op);
177
178
}

179
static int stream_wait(pa_stream *stream)
180
181
182
183
184
185
{
    pa_stream_state_t state;

    while ((state = pa_stream_get_state(stream)) != PA_STREAM_READY) {
        if (state == PA_STREAM_FAILED || state == PA_STREAM_TERMINATED)
            return -1;
186
        vlc_pa_wait();
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
    }
    return 0;
}

/* Memory free callback. The block_t address is in front of the data. */
static void data_free(void *data)
{
    block_t **pp = data, *block;

    memcpy(&block, pp - 1, sizeof (block));
    block_Release(block);
}

static void *data_convert(block_t **pp)
{
    block_t *block = *pp;
    /* In most cases, there is enough head room, and this is really cheap: */
    block = block_Realloc(block, sizeof (block), block->i_buffer);
    *pp = block;
    if (unlikely(block == NULL))
        return NULL;

    memcpy(block->p_buffer, &block, sizeof (block));
    block->p_buffer += sizeof (block);
    block->i_buffer -= sizeof (block);
    return block->p_buffer;
}

215
216
217
/**
 * Queue one audio frame to the playabck stream
 */
218
219
220
221
222
static void Play(aout_instance_t *aout)
{
    aout_sys_t *sys = aout->output.p_sys;
    pa_stream *s = sys->stream;

223
224
225
226
227
228
229
230
231
232
233
    /* This function is called exactly once per block in the output FIFO. */
    block_t *block = aout_FifoPop(&aout->output.fifo);
    assert (block != NULL);

    const void *ptr = data_convert(&block);
    if (unlikely(ptr == NULL))
        return;

    size_t len = block->i_buffer;
    mtime_t pts = block->i_pts + block->i_length;

234
235
236
237
238
    /* Note: The core already holds the output FIFO lock at this point.
     * Therefore we must not under any circumstances (try to) acquire the
     * output FIFO lock while the PulseAudio threaded main loop lock is held
     * (including from PulseAudio stream callbacks). Otherwise lock inversion
     * will take place, and sooner or later a deadlock. */
239
    vlc_pa_lock();
240

241
    if (pa_stream_is_corked(s) > 0) {
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
        /* Start or resume the stream. Zeroes are prepended to sync.
         * This does not really work because PulseAudio latency measurement is
         * garbage at start. */
        pa_operation *op;
        pa_usec_t latency;
        int negative;

        if (pa_stream_get_latency(s, &latency, &negative) == 0)
            msg_Dbg(aout, "starting with %c%"PRIu64" us latency",
                    negative ? '-' : '+', latency);
        else
            latency = negative = 0;

        mtime_t advance = block->i_pts - mdate();
        if (negative)
            advance += latency;
        else
            advance -= latency;

        if (advance > 0) {
            size_t nb = (advance * aout->output.output.i_rate) / CLOCK_FREQ;
            size_t size = aout->output.output.i_bytes_per_frame;
            float *zeroes = calloc (nb, size);

            msg_Dbg(aout, "prepending %zu zeroes", nb);
            if (likely(zeroes != NULL))
                if (pa_stream_write(s, zeroes, nb * size, free, 0,
                                    PA_SEEK_RELATIVE) < 0)
                    free(zeroes);
        }

        op = pa_stream_cork(s, 0, NULL, NULL);
        if (op != NULL)
            pa_operation_unref(op);
        op = pa_stream_trigger(s, NULL, NULL);
277
278
279
280
281
282
283
284
285
286
287
288
289
        if (op != NULL)
            pa_operation_unref(op);
        msg_Dbg(aout, "uncorking");
    }

#if 0 /* Fault injector to test underrun recovery */
    static unsigned u = 0;
    if ((++u % 500) == 0) {
        msg_Err(aout, "fault injection");
        msleep(CLOCK_FREQ*2);
    }
#endif

290
    if (pa_stream_write(s, ptr, len, data_free, 0, PA_SEEK_RELATIVE) < 0) {
291
        vlc_pa_error(aout, "cannot write", sys->context);
292
        block_Release(block);
293
294
    }

295
    vlc_pa_unlock();
296
297
}

298
299
300
/**
 * Cork or uncork the playback stream
 */
301
302
303
304
305
static void Pause(aout_instance_t *aout, bool b_paused, mtime_t i_date)
{
    aout_sys_t *sys = aout->output.p_sys;
    pa_stream *s = sys->stream;

306
307
308
    if (!b_paused)
        return; /* nothing to do - yet */

309
    vlc_pa_lock();
310

311
    pa_operation *op = pa_stream_cork(s, 1, NULL, NULL);
312
313
314
    if (op != NULL)
        pa_operation_unref(op);

315
    vlc_pa_unlock();
316
    (void) i_date;
317
318
}

319
static int VolumeSet(aout_instance_t *aout, float vol, bool mute)
320
321
322
323
324
{
    aout_sys_t *sys = aout->output.p_sys;
    pa_operation *op;

    uint32_t idx = pa_stream_get_index(sys->stream);
325
    pa_volume_t volume = pa_sw_volume_from_linear(vol);
326
    pa_cvolume cvolume;
327

328
329
    /* TODO: do not ruin the channel balance (if set outside VLC) */
    /* TODO: notify UI about volume changes by other PulseAudio clients */
330
    pa_cvolume_set(&sys->cvolume, sys->cvolume.channels, volume);
331
332
    pa_sw_cvolume_multiply_scalar(&cvolume, &sys->cvolume, sys->base_volume);
    assert(pa_cvolume_valid(&cvolume));
333

334
    vlc_pa_lock();
335
    op = pa_context_set_sink_input_volume(sys->context, idx, &cvolume, NULL, NULL);
336
337
    if (likely(op != NULL))
        pa_operation_unref(op);
338
    op = pa_context_set_sink_input_mute(sys->context, idx, mute, NULL, NULL);
339
340
    if (likely(op != NULL))
        pa_operation_unref(op);
341
    vlc_pa_unlock();
342
343
344

    return 0;
}
345

346
347
348
349
350
351
352
353
354
355
static int StreamMove(vlc_object_t *obj, const char *varname, vlc_value_t old,
                      vlc_value_t val, void *userdata)
{
    aout_instance_t *aout = (aout_instance_t *)obj;
    aout_sys_t *sys = aout->output.p_sys;
    pa_stream *s = userdata;
    pa_operation *op;
    uint32_t idx = pa_stream_get_index(s);
    uint32_t sink_idx = val.i_int;

356
357
    (void) varname; (void) old;

358
    vlc_pa_lock();
359
360
    op = pa_context_move_sink_input_by_index(sys->context, idx, sink_idx,
                                             NULL, NULL);
361
362
363
364
    if (likely(op != NULL)) {
        pa_operation_unref(op);
        msg_Dbg(aout, "moving to sink %"PRIu32, sink_idx);
    } else
365
366
        vlc_pa_error(obj, "cannot move sink", sys->context);
    vlc_pa_unlock();
367
368

    return (op != NULL) ? VLC_SUCCESS : VLC_EGENERIC;
369
370
371
}


372
373
374
/**
 * Create a PulseAudio playback stream, a.k.a. a sink input.
 */
375
static int Open(vlc_object_t *obj)
376
{
377
    aout_instance_t *aout = (aout_instance_t *)obj;
378
    pa_operation *op;
379

380
381
    /* Sample format specification */
    struct pa_sample_spec ss;
Danny Wood's avatar
Danny Wood committed
382

383
    switch(aout->output.output.i_format)
Danny Wood's avatar
Danny Wood committed
384
    {
385
        case VLC_CODEC_F64B:
386
            aout->output.output.i_format = VLC_CODEC_F32B;
387
388
        case VLC_CODEC_F32B:
            ss.format = PA_SAMPLE_FLOAT32BE;
389
            break;
390
        case VLC_CODEC_F64L:
391
            aout->output.output.i_format = VLC_CODEC_F32L;
392
393
        case VLC_CODEC_F32L:
            ss.format = PA_SAMPLE_FLOAT32LE;
394
            break;
395
        case VLC_CODEC_FI32:
396
            aout->output.output.i_format = VLC_CODEC_FL32;
397
            ss.format = PA_SAMPLE_FLOAT32NE;
398
            break;
399
400
        case VLC_CODEC_S32B:
            ss.format = PA_SAMPLE_S32BE;
401
            break;
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
        case VLC_CODEC_S32L:
            ss.format = PA_SAMPLE_S32LE;
            break;
        case VLC_CODEC_S24B:
            ss.format = PA_SAMPLE_S24BE;
            break;
        case VLC_CODEC_S24L:
            ss.format = PA_SAMPLE_S24LE;
            break;
        case VLC_CODEC_S16B:
            ss.format = PA_SAMPLE_S16BE;
            break;
        case VLC_CODEC_S16L:
            ss.format = PA_SAMPLE_S16LE;
            break;
        case VLC_CODEC_S8:
418
            aout->output.output.i_format = VLC_CODEC_U8;
419
420
        case VLC_CODEC_U8:
            ss.format = PA_SAMPLE_U8;
421
422
            break;
        default:
423
424
            if (HAVE_FPU)
            {
425
                aout->output.output.i_format = VLC_CODEC_FL32;
426
427
428
429
                ss.format = PA_SAMPLE_FLOAT32NE;
            }
            else
            {
430
                aout->output.output.i_format = VLC_CODEC_S16N;
431
432
433
                ss.format = PA_SAMPLE_S16NE;
            }
            break;
Danny Wood's avatar
Danny Wood committed
434
435
    }

436
437
    ss.rate = aout->output.output.i_rate;
    ss.channels = aout_FormatNbChannels(&aout->output.output);
438
    if (!pa_sample_spec_valid(&ss)) {
439
        msg_Err(aout, "unsupported sample specification");
440
        return VLC_EGENERIC;
441
    }
442

443
    /* Channel mapping (order defined in vlc_aout.h) */
444
445
446
    struct pa_channel_map map;
    map.channels = 0;

447
    if (aout->output.output.i_physical_channels & AOUT_CHAN_LEFT)
448
        map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_LEFT;
449
    if (aout->output.output.i_physical_channels & AOUT_CHAN_RIGHT)
450
        map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_RIGHT;
451
    if (aout->output.output.i_physical_channels & AOUT_CHAN_MIDDLELEFT)
452
        map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_LEFT;
453
    if (aout->output.output.i_physical_channels & AOUT_CHAN_MIDDLERIGHT)
454
        map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_RIGHT;
455
456
457
458
459
460
461
462
463
464
465
466
467
    if (aout->output.output.i_physical_channels & AOUT_CHAN_REARLEFT)
        map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_LEFT;
    if (aout->output.output.i_physical_channels & AOUT_CHAN_REARRIGHT)
        map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_RIGHT;
    if (aout->output.output.i_physical_channels & AOUT_CHAN_REARCENTER)
        map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_CENTER;
    if (aout->output.output.i_physical_channels & AOUT_CHAN_CENTER)
    {
        if (ss.channels == 1)
            map.map[map.channels++] = PA_CHANNEL_POSITION_MONO;
        else
            map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_CENTER;
    }
468
    if (aout->output.output.i_physical_channels & AOUT_CHAN_LFE)
469
470
471
472
        map.map[map.channels++] = PA_CHANNEL_POSITION_LFE;

    for (unsigned i = 0; map.channels < ss.channels; i++) {
        map.map[map.channels++] = PA_CHANNEL_POSITION_AUX0 + i;
473
        msg_Warn(aout, "mapping channel %"PRIu8" to AUX%u", map.channels, i);
474
    }
475

476
    if (!pa_channel_map_valid(&map)) {
477
        msg_Err(aout, "unsupported channel map");
478
479
480
        return VLC_EGENERIC;
    } else {
        const char *name = pa_channel_map_to_pretty_name(&map);
481
        msg_Dbg(aout, "using %s channel map", (name != NULL) ? name : "?");
482
    }
483

484
    /* Stream parameters */
485
486
487
    const pa_stream_flags_t flags = PA_STREAM_START_CORKED
                                  //| PA_STREAM_INTERPOLATE_TIMING
                                  | PA_STREAM_AUTO_TIMING_UPDATE;
488

489
    const uint32_t byterate = pa_bytes_per_second(&ss);
490
    struct pa_buffer_attr attr;
491
    attr.maxlength = -1;
492
493
494
495
496
497
498
    /* PulseAudio assumes that tlength bytes are available in the buffer. Thus
     * we need to be conservative and set the minimum value that the VLC
     * audio decoder thread warrants. Otherwise, PulseAudio buffers will
     * underrun on hardware with large buffers. VLC keeps at least
     * AOUT_MIN_PREPARE and at most AOUT_MAX_PREPARE worth of audio buffers.
     * TODO? tlength could be adaptively increased to reduce wakeups. */
    attr.tlength = byterate * AOUT_MIN_PREPARE_TIME / CLOCK_FREQ;
499
    attr.prebuf = 0; /* trigger manually */
500
501
    attr.minreq = -1;
    attr.fragsize = 0; /* not used for output */
502

503
    /* Allocate structures */
504
    aout_sys_t *sys = malloc(sizeof(*sys));
505
506
    if (unlikely(sys == NULL))
        return VLC_ENOMEM;
507
508
509
510
511
512
513
514

    pa_context *ctx = vlc_pa_connect (obj);
    if (ctx == NULL)
    {
        free (sys);
        return VLC_EGENERIC;
    }

515
    aout->output.p_sys = sys;
516
    sys->stream = NULL;
517
    sys->context = ctx;
518

519
    /* Channel volume */
520
    sys->base_volume = PA_VOLUME_NORM;
521
522
    pa_cvolume_set(&sys->cvolume, ss.channels, PA_VOLUME_NORM);

523
    vlc_pa_lock();
524
525
526
    /* Create a playback stream */
    pa_stream *s = pa_stream_new(ctx, "audio stream", &ss, &map);
    if (s == NULL) {
527
        vlc_pa_error(obj, "stream creation failure", ctx);
528
        goto fail;
529
    }
530
    sys->stream = s;
531
    pa_stream_set_state_callback(s, stream_state_cb, NULL);
532
    pa_stream_set_moved_callback(s, stream_moved_cb, aout);
533
534
535
536
    pa_stream_set_overflow_callback(s, stream_overflow_cb, aout);
    pa_stream_set_started_callback(s, stream_started_cb, aout);
    pa_stream_set_suspended_callback(s, stream_suspended_cb, aout);
    pa_stream_set_underflow_callback(s, stream_underflow_cb, aout);
537

538
    if (pa_stream_connect_playback(s, NULL, &attr, flags, NULL, NULL) < 0
539
540
     || stream_wait(s)) {
        vlc_pa_error(obj, "stream connection failure", ctx);
541
        goto fail;
542
543
    }

544
545
    const struct pa_buffer_attr *pba = pa_stream_get_buffer_attr(s);
    msg_Dbg(aout, "using buffer metrics: maxlength=%u, tlength=%u, "
546
547
            "prebuf=%u, minreq=%u",
            pba->maxlength, pba->tlength, pba->prebuf, pba->minreq);
548

549
    aout->output.i_nb_samples = pba->minreq / pa_frame_size(&ss);
550
551
552
553
554
555
556
557
558
559

    var_Create(aout, "audio-device", VLC_VAR_INTEGER|VLC_VAR_HASCHOICE);
    var_Change(aout, "audio-device", VLC_VAR_SETTEXT,
               &(vlc_value_t){ .psz_string = (char *)_("Audio device") },
               NULL);
    var_AddCallback (aout, "audio-device", StreamMove, s);
    op = pa_context_get_sink_info_list(ctx, sink_list_cb, aout);
    /* We may need to wait for completion... once LibVLC supports this */
    if (op != NULL)
        pa_operation_unref(op);
560
    stream_moved_cb(s, aout);
561
    vlc_pa_unlock();
562
563

    aout->output.pf_play = Play;
564
    aout->output.pf_pause = Pause;
565
    aout->output.pf_volume_set = VolumeSet;
566
567
568
    return VLC_SUCCESS;

fail:
569
    vlc_pa_unlock();
570
    Close(obj);
571
572
573
    return VLC_EGENERIC;
}

574
575
576
/**
 * Removes a PulseAudio playback stream
 */
577
static void Close (vlc_object_t *obj)
578
{
579
    aout_instance_t *aout = (aout_instance_t *)obj;
580
581
582
583
    aout_sys_t *sys = aout->output.p_sys;
    pa_context *ctx = sys->context;
    pa_stream *s = sys->stream;

584
585
586
587
588
589
    if (s != NULL) {
        /* The callback takes mainloop lock, so it CANNOT be held here! */
        var_DelCallback (aout, "audio-device", StreamMove, s);
        var_Destroy (aout, "audio-device");
    }

590
    vlc_pa_lock();
591
592
593
594
595
596
597
598
599
600
601
    if (s != NULL) {
        pa_operation *op;

        op = pa_stream_flush(s, NULL, NULL);
        if (op != NULL)
            pa_operation_unref(op);
        op = pa_stream_drain(s, NULL, NULL);
        if (op != NULL)
            pa_operation_unref(op);
        pa_stream_disconnect(s);
        pa_stream_unref(s);
602
    }
603
604
605
    vlc_pa_unlock();

    vlc_pa_disconnect(obj, ctx);
606
    free(sys);
607
}