Something went wrong. Try again.
What happens if I push all of nixpkgs
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801diff --git a/audio.c b/audio.c--- a/audio.c+++ b/audio.c@@ -32,6 +32,9 @@ #include "log.h" #include "lists.h" +#ifdef HAVE_PULSE+# include "pulse.h"+#endif #ifdef HAVE_OSS # include "oss.h" #endif@@ -893,6 +896,15 @@ } #endif +#ifdef HAVE_PULSE+ if (!strcasecmp(name, "pulseaudio")) {+ pulse_funcs (funcs);+ printf ("Trying PulseAudio...\n");+ if (funcs->init(&hw_caps))+ return;+ }+#endif+ #ifdef HAVE_OSS if (!strcasecmp(name, "oss")) { oss_funcs (funcs);diff --git a/configure.in b/configure.in--- a/configure.in+++ b/configure.in@@ -162,6 +162,21 @@ AC_MSG_ERROR([BerkeleyDB (libdb) not found.])) fi +AC_ARG_WITH(pulse, AS_HELP_STRING(--without-pulse,+ Compile without PulseAudio support.))++if test "x$with_pulse" != "xno"+then+ PKG_CHECK_MODULES(PULSE, [libpulse],+ [SOUND_DRIVERS="$SOUND_DRIVERS PULSE"+ EXTRA_OBJS="$EXTRA_OBJS pulse.o"+ AC_DEFINE([HAVE_PULSE], 1, [Define if you have PulseAudio.])+ EXTRA_LIBS="$EXTRA_LIBS $PULSE_LIBS"+ CFLAGS="$CFLAGS $PULSE_CFLAGS"],+ [true])+fi++ AC_ARG_WITH(oss, AS_HELP_STRING([--without-oss], [Compile without OSS support])) diff --git a/options.c b/options.c--- a/options.c+++ b/options.c@@ -572,10 +572,11 @@ #ifdef OPENBSD add_list ("SoundDriver", "SNDIO:JACK:OSS",- CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null");+ CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null");+ #else add_list ("SoundDriver", "Jack:ALSA:OSS",- CHECK_DISCRETE(5), "SNDIO", "Jack", "ALSA", "OSS", "null");+ CHECK_DISCRETE(5), "SNDIO", "PulseAudio", "Jack", "ALSA", "OSS", "null"); #endif add_str ("JackClientName", "moc", CHECK_NONE);diff --git a/pulse.c b/pulse.cnew file mode 100644--- /dev/null+++ b/pulse.c@@ -0,0 +1,705 @@+/*+ * MOC - music on console+ * Copyright (C) 2011 Marien Zwart <marienz@marienz.net>+ *+ * 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.+ *+ */++/* PulseAudio backend.+ *+ * FEATURES:+ *+ * Does not autostart a PulseAudio server, but uses an already-started+ * one, which should be better than alsa-through-pulse.+ *+ * Supports control of either our stream's or our entire sink's volume+ * while we are actually playing. Volume control while paused is+ * intentionally unsupported: the PulseAudio documentation strongly+ * suggests not passing in an initial volume when creating a stream+ * (allowing the server to track this instead), and we do not know+ * which sink to control if we do not have a stream open.+ *+ * IMPLEMENTATION:+ *+ * Most client-side (resource allocation) errors are fatal. Failure to+ * create a server context or stream is not fatal (and MOC should cope+ * with these failures too), but server communication failures later+ * on are currently not handled (MOC has no great way for us to tell+ * it we no longer work, and I am not sure if attempting to reconnect+ * is worth it or even a good idea).+ *+ * The pulse "simple" API is too simple: it combines connecting to the+ * server and opening a stream into one operation, while I want to+ * connect to the server when MOC starts (and fall back to a different+ * backend if there is no server), and I cannot open a stream at that+ * time since I do not know the audio format yet.+ *+ * PulseAudio strongly recommends we use a high-latency connection,+ * which the MOC frontend code might not expect from its audio+ * backend. We'll see.+ *+ * We map MOC's percentage volumes linearly to pulse's PA_VOLUME_MUTED+ * (0) .. PA_VOLUME_NORM range. This is what the PulseAudio docs recommend+ * ( http://pulseaudio.org/wiki/WritingVolumeControlUIs ). It does mean+ * PulseAudio volumes above PA_VOLUME_NORM do not work well with MOC.+ *+ * Comments in audio.h claim "All functions are executed only by one+ * thread" (referring to the function in the hw_funcs struct). This is+ * a blatant lie. Most of them are invoked off the "output buffer"+ * thread (out_buf.c) but at least the "playing" thread (audio.c)+ * calls audio_close which calls our close function. We can mostly+ * ignore this problem because we serialize on the pulseaudio threaded+ * mainloop lock. But it does mean that functions that are normally+ * only called between open and close (like reset) are sometimes+ * called without us having a stream. Bulletproof, therefore:+ * serialize setting/unsetting our global stream using the threaded+ * mainloop lock, and check for that stream being non-null before+ * using it.+ *+ * I am not convinced there are no further dragons lurking here: can+ * the "playing" thread(s) close and reopen our output stream while+ * the "output buffer" thread is sending output there? We can bail if+ * our stream is simply closed, but we do not currently detect it+ * being reopened and no longer using the same sample format, which+ * might have interesting results...+ *+ * Also, read_mixer is called from the main server thread (handling+ * commands). This crashed me once when it got at a stream that was in+ * the "creating" state and therefore did not have a valid stream+ * index yet. Fixed by only assigning to the stream global when the+ * stream is valid.+ */++#ifdef HAVE_CONFIG_H+# include "config.h"+#endif++#define DEBUG++#include <pulse/pulseaudio.h>+#include "common.h"+#include "log.h"+#include "audio.h"+++/* The pulse mainloop and context are initialized in pulse_init and+ * destroyed in pulse_shutdown.+ */+static pa_threaded_mainloop *mainloop = NULL;+static pa_context *context = NULL;++/* The stream is initialized in pulse_open and destroyed in pulse_close. */+static pa_stream *stream = NULL;++static int showing_sink_volume = 0;++/* Callbacks that do nothing but wake up the mainloop. */++static void context_state_callback (pa_context *context ATTR_UNUSED,+ void *userdata)+{+ pa_threaded_mainloop *m = userdata;++ pa_threaded_mainloop_signal (m, 0);+}++static void stream_state_callback (pa_stream *stream ATTR_UNUSED,+ void *userdata)+{+ pa_threaded_mainloop *m = userdata;++ pa_threaded_mainloop_signal (m, 0);+}++static void stream_write_callback (pa_stream *stream ATTR_UNUSED,+ size_t nbytes ATTR_UNUSED, void *userdata)+{+ pa_threaded_mainloop *m = userdata;++ pa_threaded_mainloop_signal (m, 0);+}++/* Initialize pulse mainloop and context. Failure to connect to the+ * pulse daemon is nonfatal, everything else is fatal (as it+ * presumably means we ran out of resources).+ */+static int pulse_init (struct output_driver_caps *caps)+{+ pa_context *c;+ pa_proplist *proplist;++ assert (!mainloop);+ assert (!context);++ mainloop = pa_threaded_mainloop_new ();+ if (!mainloop)+ fatal ("Cannot create PulseAudio mainloop");++ if (pa_threaded_mainloop_start (mainloop) < 0)+ fatal ("Cannot start PulseAudio mainloop");++ /* TODO: possibly add more props.+ *+ * There are a few we could set in proplist.h but nothing I+ * expect to be very useful.+ *+ * http://pulseaudio.org/wiki/ApplicationProperties recommends+ * setting at least application.name, icon.name and media.role.+ *+ * No need to set application.name here, the name passed to+ * pa_context_new_with_proplist overrides it.+ */+ proplist = pa_proplist_new ();+ if (!proplist)+ fatal ("Cannot allocate PulseAudio proplist");++ pa_proplist_sets (proplist,+ PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION);+ pa_proplist_sets (proplist, PA_PROP_MEDIA_ROLE, "music");+ pa_proplist_sets (proplist, PA_PROP_APPLICATION_ID, "net.daper.moc");++ pa_threaded_mainloop_lock (mainloop);++ c = pa_context_new_with_proplist (+ pa_threaded_mainloop_get_api (mainloop),+ PACKAGE_NAME, proplist);+ pa_proplist_free (proplist);++ if (!c)+ fatal ("Cannot allocate PulseAudio context");++ pa_context_set_state_callback (c, context_state_callback, mainloop);++ /* Ignore return value, rely on state being set properly */+ pa_context_connect (c, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);++ while (1) {+ pa_context_state_t state = pa_context_get_state (c);++ if (state == PA_CONTEXT_READY)+ break;++ if (!PA_CONTEXT_IS_GOOD (state)) {+ error ("PulseAudio connection failed: %s",+ pa_strerror (pa_context_errno (c)));++ goto unlock_and_fail;+ }++ debug ("waiting for context to become ready...");+ pa_threaded_mainloop_wait (mainloop);+ }++ /* Only set the global now that the context is actually ready */+ context = c;++ pa_threaded_mainloop_unlock (mainloop);++ /* We just make up the hardware capabilities, since pulse is+ * supposed to be abstracting these out. Assume pulse will+ * deal with anything we want to throw at it, and that we will+ * only want mono or stereo audio.+ */+ caps->min_channels = 1;+ caps->max_channels = 2;+ caps->formats = (SFMT_S8 | SFMT_S16 | SFMT_S32 |+ SFMT_FLOAT | SFMT_BE | SFMT_LE);++ return 1;++unlock_and_fail:++ pa_context_unref (c);++ pa_threaded_mainloop_unlock (mainloop);++ pa_threaded_mainloop_stop (mainloop);+ pa_threaded_mainloop_free (mainloop);+ mainloop = NULL;++ return 0;+}++static void pulse_shutdown (void)+{+ pa_threaded_mainloop_lock (mainloop);++ pa_context_disconnect (context);+ pa_context_unref (context);+ context = NULL;++ pa_threaded_mainloop_unlock (mainloop);++ pa_threaded_mainloop_stop (mainloop);+ pa_threaded_mainloop_free (mainloop);+ mainloop = NULL;+}++static int pulse_open (struct sound_params *sound_params)+{+ pa_sample_spec ss;+ pa_buffer_attr ba;+ pa_stream *s;++ assert (!stream);+ /* Initialize everything to -1, which in practice gets us+ * about 2 seconds of latency (which is fine). This is not the+ * same as passing NULL for this struct, which gets us an+ * unnecessarily short alsa-like latency.+ */+ ba.fragsize = (uint32_t) -1;+ ba.tlength = (uint32_t) -1;+ ba.prebuf = (uint32_t) -1;+ ba.minreq = (uint32_t) -1;+ ba.maxlength = (uint32_t) -1;++ ss.channels = sound_params->channels;+ ss.rate = sound_params->rate;+ switch (sound_params->fmt) {+ case SFMT_U8:+ ss.format = PA_SAMPLE_U8;+ break;+ case SFMT_S16 | SFMT_LE:+ ss.format = PA_SAMPLE_S16LE;+ break;+ case SFMT_S16 | SFMT_BE:+ ss.format = PA_SAMPLE_S16BE;+ break;+ case SFMT_FLOAT | SFMT_LE:+ ss.format = PA_SAMPLE_FLOAT32LE;+ break;+ case SFMT_FLOAT | SFMT_BE:+ ss.format = PA_SAMPLE_FLOAT32BE;+ break;+ case SFMT_S32 | SFMT_LE:+ ss.format = PA_SAMPLE_S32LE;+ break;+ case SFMT_S32 | SFMT_BE:+ ss.format = PA_SAMPLE_S32BE;+ break;++ default:+ fatal ("pulse: got unrequested format");+ }++ debug ("opening stream");++ pa_threaded_mainloop_lock (mainloop);++ /* TODO: figure out if there are useful stream properties to set.+ *+ * I do not really see any in proplist.h that we can set from+ * here (there are media title/artist/etc props but we do not+ * have that data available here).+ */+ s = pa_stream_new (context, "music", &ss, NULL);+ if (!s)+ fatal ("pulse: stream allocation failed");++ pa_stream_set_state_callback (s, stream_state_callback, mainloop);+ pa_stream_set_write_callback (s, stream_write_callback, mainloop);++ /* Ignore return value, rely on failed stream state instead. */+ pa_stream_connect_playback (+ s, NULL, &ba,+ PA_STREAM_INTERPOLATE_TIMING |+ PA_STREAM_AUTO_TIMING_UPDATE |+ PA_STREAM_ADJUST_LATENCY,+ NULL, NULL);++ while (1) {+ pa_stream_state_t state = pa_stream_get_state (s);++ if (state == PA_STREAM_READY)+ break;++ if (!PA_STREAM_IS_GOOD (state)) {+ error ("PulseAudio stream connection failed");++ goto fail;+ }++ debug ("waiting for stream to become ready...");+ pa_threaded_mainloop_wait (mainloop);+ }++ /* Only set the global stream now that it is actually ready */+ stream = s;++ pa_threaded_mainloop_unlock (mainloop);++ return 1;++fail:+ pa_stream_unref (s);++ pa_threaded_mainloop_unlock (mainloop);+ return 0;+}++static void pulse_close (void)+{+ debug ("closing stream");++ pa_threaded_mainloop_lock (mainloop);++ pa_stream_disconnect (stream);+ pa_stream_unref (stream);+ stream = NULL;++ pa_threaded_mainloop_unlock (mainloop);+}++static int pulse_play (const char *buff, const size_t size)+{+ size_t offset = 0;++ debug ("Got %d bytes to play", (int)size);++ pa_threaded_mainloop_lock (mainloop);++ /* The buffer is usually writable when we get here, and there+ * are usually few (if any) writes after the first one. So+ * there is no point in doing further writes directly from the+ * callback: we can just do all writes from this thread.+ */++ /* Break out of the loop if some other thread manages to close+ * our stream underneath us.+ */+ while (stream) {+ size_t towrite = MIN(pa_stream_writable_size (stream),+ size - offset);+ debug ("writing %d bytes", (int)towrite);++ /* We have no working way of dealing with errors+ * (see below). */+ if (pa_stream_write(stream, buff + offset, towrite,+ NULL, 0, PA_SEEK_RELATIVE))+ error ("pa_stream_write failed");++ offset += towrite;++ if (offset >= size)+ break;++ pa_threaded_mainloop_wait (mainloop);+ }++ pa_threaded_mainloop_unlock (mainloop);++ debug ("Done playing!");++ /* We should always return size, calling code does not deal+ * well with anything else. Only read the rest if you want to+ * know why.+ *+ * The output buffer reader thread (out_buf.c:read_thread)+ * repeatedly loads some 64k/0.1s of audio into a buffer on+ * the stack, then calls audio_send_pcm repeatedly until this+ * entire buffer has been processed (similar to the loop in+ * this function). audio_send_pcm applies the softmixer and+ * equalizer, then feeds the result to this function, passing+ * through our return value.+ *+ * So if we return less than size the equalizer/softmixer is+ * re-applied to the remaining data, which is silly. Also,+ * audio_send_pcm checks for our return value being zero and+ * calls fatal() if it is, so try to always process *some*+ * data. Also, out_buf.c uses the return value of this+ * function from the last run through its inner loop to update+ * its time attribute, which means it will be interestingly+ * off if that loop ran more than once.+ *+ * Oh, and alsa.c seems to think it can return -1 to indicate+ * failure, which will cause out_buf.c to rewind its buffer+ * (to before its start, usually).+ */+ return size;+}++static void volume_cb (const pa_cvolume *v, void *userdata)+{+ int *result = userdata;++ if (v)+ *result = 100 * pa_cvolume_avg (v) / PA_VOLUME_NORM;++ pa_threaded_mainloop_signal (mainloop, 0);+}++static void sink_volume_cb (pa_context *c ATTR_UNUSED,+ const pa_sink_info *i, int eol ATTR_UNUSED,+ void *userdata)+{+ volume_cb (i ? &i->volume : NULL, userdata);+}++static void sink_input_volume_cb (pa_context *c ATTR_UNUSED,+ const pa_sink_input_info *i,+ int eol ATTR_UNUSED,+ void *userdata ATTR_UNUSED)+{+ volume_cb (i ? &i->volume : NULL, userdata);+}++static int pulse_read_mixer (void)+{+ pa_operation *op;+ int result = 0;++ debug ("read mixer");++ pa_threaded_mainloop_lock (mainloop);++ if (stream) {+ if (showing_sink_volume)+ op = pa_context_get_sink_info_by_index (+ context, pa_stream_get_device_index (stream),+ sink_volume_cb, &result);+ else+ op = pa_context_get_sink_input_info (+ context, pa_stream_get_index (stream),+ sink_input_volume_cb, &result);++ while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)+ pa_threaded_mainloop_wait (mainloop);++ pa_operation_unref (op);+ }++ pa_threaded_mainloop_unlock (mainloop);++ return result;+}++static void pulse_set_mixer (int vol)+{+ pa_cvolume v;+ pa_operation *op;++ /* Setting volume for one channel does the right thing. */+ pa_cvolume_set(&v, 1, vol * PA_VOLUME_NORM / 100);++ pa_threaded_mainloop_lock (mainloop);++ if (stream) {+ if (showing_sink_volume)+ op = pa_context_set_sink_volume_by_index (+ context, pa_stream_get_device_index (stream),+ &v, NULL, NULL);+ else+ op = pa_context_set_sink_input_volume (+ context, pa_stream_get_index (stream),+ &v, NULL, NULL);++ pa_operation_unref (op);+ }++ pa_threaded_mainloop_unlock (mainloop);+}++static int pulse_get_buff_fill (void)+{+ /* This function is problematic. MOC uses it to for the "time+ * remaining" in the UI, but calls it more than once per+ * second (after each chunk of audio played, not for each+ * playback time update). We have to be fairly accurate here+ * for that time remaining to not jump weirdly. But PulseAudio+ * cannot give us a 100% accurate value here, as it involves a+ * server roundtrip. And if we call this a lot it suggests+ * switching to a mode where the value is interpolated, making+ * it presumably more inaccurate (see the flags we pass to+ * pa_stream_connect_playback).+ *+ * MOC also contains what I believe to be a race: it calls+ * audio_get_buff_fill "soon" (after playing the first chunk)+ * after starting playback of the next song, at which point we+ * still have part of the previous song buffered. This means+ * our position into the new song is negative, which fails an+ * assert (in out_buf.c:out_buf_time_get). There is no sane+ * way for us to detect this condition. I believe no other+ * backend triggers this because the assert sits after an+ * implicit float -> int seconds conversion, which means we+ * have to be off by at least an entire second to get a+ * negative value, and none of the other backends have buffers+ * that large (alsa buffers are supposedly a few 100 ms).+ */+ pa_usec_t buffered_usecs = 0;+ int buffered_bytes = 0;++ pa_threaded_mainloop_lock (mainloop);++ /* Using pa_stream_get_timing_info and returning the distance+ * between write_index and read_index would be more obvious,+ * but because of how the result is actually used I believe+ * using the latency value is slightly more correct, and it+ * makes the following crash-avoidance hack more obvious.+ */++ /* This function will frequently fail the first time we call+ * it (pulse does not have the requested data yet). We ignore+ * that and just return 0.+ *+ * Deal with stream being NULL too, just in case this is+ * called in a racy fashion similar to how reset() is.+ */+ if (stream &&+ pa_stream_get_latency (stream, &buffered_usecs, NULL) >= 0) {+ /* Crash-avoidance HACK: floor our latency to at most+ * 1 second. It is usually more, but reporting that at+ * the start of playback crashes MOC, and we cannot+ * sanely detect when reporting it is safe.+ */+ if (buffered_usecs > 1000000)+ buffered_usecs = 1000000;++ buffered_bytes = pa_usec_to_bytes (+ buffered_usecs,+ pa_stream_get_sample_spec (stream));+ }++ pa_threaded_mainloop_unlock (mainloop);++ debug ("buffer fill: %d usec / %d bytes",+ (int) buffered_usecs, (int) buffered_bytes);++ return buffered_bytes;+}++static void flush_callback (pa_stream *s ATTR_UNUSED, int success,+ void *userdata)+{+ int *result = userdata;++ *result = success;++ pa_threaded_mainloop_signal (mainloop, 0);+}++static int pulse_reset (void)+{+ pa_operation *op;+ int result = 0;++ debug ("reset requested");++ pa_threaded_mainloop_lock (mainloop);++ /* We *should* have a stream here, but MOC is racy, so bulletproof */+ if (stream) {+ op = pa_stream_flush (stream, flush_callback, &result);++ while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)+ pa_threaded_mainloop_wait (mainloop);++ pa_operation_unref (op);+ } else+ logit ("pulse_reset() called without a stream");++ pa_threaded_mainloop_unlock (mainloop);++ return result;+}++static int pulse_get_rate (void)+{+ /* This is called once right after open. Do not bother making+ * this fast. */++ int result;++ pa_threaded_mainloop_lock (mainloop);++ if (stream)+ result = pa_stream_get_sample_spec (stream)->rate;+ else {+ error ("get_rate called without a stream");+ result = 0;+ }++ pa_threaded_mainloop_unlock (mainloop);++ return result;+}++static void pulse_toggle_mixer_channel (void)+{+ showing_sink_volume = !showing_sink_volume;+}++static void sink_name_cb (pa_context *c ATTR_UNUSED,+ const pa_sink_info *i, int eol ATTR_UNUSED,+ void *userdata)+{+ char **result = userdata;++ if (i && !*result)+ *result = xstrdup (i->name);++ pa_threaded_mainloop_signal (mainloop, 0);+}++static void sink_input_name_cb (pa_context *c ATTR_UNUSED,+ const pa_sink_input_info *i,+ int eol ATTR_UNUSED,+ void *userdata)+{+ char **result = userdata;++ if (i && !*result)+ *result = xstrdup (i->name);++ pa_threaded_mainloop_signal (mainloop, 0);+}++static char *pulse_get_mixer_channel_name (void)+{+ char *result = NULL;+ pa_operation *op;++ pa_threaded_mainloop_lock (mainloop);++ if (stream) {+ if (showing_sink_volume)+ op = pa_context_get_sink_info_by_index (+ context, pa_stream_get_device_index (stream),+ sink_name_cb, &result);+ else+ op = pa_context_get_sink_input_info (+ context, pa_stream_get_index (stream),+ sink_input_name_cb, &result);++ while (pa_operation_get_state (op) == PA_OPERATION_RUNNING)+ pa_threaded_mainloop_wait (mainloop);++ pa_operation_unref (op);+ }++ pa_threaded_mainloop_unlock (mainloop);++ if (!result)+ result = xstrdup ("disconnected");++ return result;+}++void pulse_funcs (struct hw_funcs *funcs)+{+ funcs->init = pulse_init;+ funcs->shutdown = pulse_shutdown;+ funcs->open = pulse_open;+ funcs->close = pulse_close;+ funcs->play = pulse_play;+ funcs->read_mixer = pulse_read_mixer;+ funcs->set_mixer = pulse_set_mixer;+ funcs->get_buff_fill = pulse_get_buff_fill;+ funcs->reset = pulse_reset;+ funcs->get_rate = pulse_get_rate;+ funcs->toggle_mixer_channel = pulse_toggle_mixer_channel;+ funcs->get_mixer_channel_name = pulse_get_mixer_channel_name;+}diff --git a/pulse.h b/pulse.hnew file mode 100644--- /dev/null+++ b/pulse.h@@ -0,0 +1,14 @@+#ifndef PULSE_H+#define PULSE_H++#ifdef __cplusplus+extern "C" {+#endif++void pulse_funcs (struct hw_funcs *funcs);++#ifdef __cplusplus+}+#endif++#endif