/* PulseAudio output plugin for DeaDBeeF Player Copyright (C) 2011 Jan D. Behrens Copyright (C) 2010-2012 Alexey Yakovenko Copyright (C) 2010 Anton Novikov 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, see . */ #ifdef HAVE_CONFIG_H # include "../../config.h" #endif #include #include #include #ifdef __linux__ #include #endif #include #include #include "../../deadbeef.h" //#define trace(...) { fprintf(stdout, __VA_ARGS__); } #define trace(fmt,...) static DB_output_t plugin; DB_functions_t * deadbeef; #define CONFSTR_PULSE_SERVERADDR "pulse.serveraddr" #define CONFSTR_PULSE_BUFFERSIZE "pulse.buffersize" static intptr_t pulse_tid; static int pulse_terminate; static pa_simple *s; static pa_sample_spec ss; static ddb_waveformat_t requested_fmt; static int state; static uintptr_t mutex; static int buffer_size; static void pulse_thread(void *context); static void pulse_callback(char *stream, int len); static int pulse_init(); static int pulse_free(); static int pulse_setformat(ddb_waveformat_t *fmt); static int pulse_play(); static int pulse_stop(); static int pulse_pause(); static int pulse_unpause(); static int pulse_set_spec(ddb_waveformat_t *fmt) { memcpy (&plugin.fmt, fmt, sizeof (ddb_waveformat_t)); if (!plugin.fmt.channels) { // generic format plugin.fmt.bps = 16; plugin.fmt.is_float = 0; plugin.fmt.channels = 2; plugin.fmt.samplerate = 44100; plugin.fmt.channelmask = 3; } trace ("format %dbit %s %dch %dHz channelmask=%X\n", plugin.fmt.bps, plugin.fmt.is_float ? "float" : "int", plugin.fmt.channels, plugin.fmt.samplerate, plugin.fmt.channelmask); ss.channels = plugin.fmt.channels; // Try to auto-configure the channel map, see for details pa_channel_map channel_map; pa_channel_map_init_extend(&channel_map, ss.channels, PA_CHANNEL_MAP_WAVEEX); trace ("pulse: channels: %d\n", ss.channels); // Read samplerate from config //ss.rate = deadbeef->conf_get_int(CONFSTR_PULSE_SAMPLERATE, 44100); ss.rate = plugin.fmt.samplerate; trace ("pulse: samplerate: %d\n", ss.rate); switch (plugin.fmt.bps) { case 8: ss.format = PA_SAMPLE_U8; break; case 16: ss.format = PA_SAMPLE_S16LE; break; case 24: ss.format = PA_SAMPLE_S24LE; break; case 32: if (plugin.fmt.is_float) { ss.format = PA_SAMPLE_FLOAT32LE; } else { ss.format = PA_SAMPLE_S32LE; } break; default: return -1; }; if (s) { pa_simple_free(s); } pa_buffer_attr * attr = NULL; //attr->maxlength = Maximum length of the buffer. //attr->tlength = Playback only: target length of the buffer. //attr->prebuf = Playback only: pre-buffering. //attr->minreq = Playback only: minimum request. //attr->fragsize = Recording only: fragment size. buffer_size = deadbeef->conf_get_int(CONFSTR_PULSE_BUFFERSIZE, 4096); // TODO: where list of all available devices? add this option to config too.. char * dev = NULL; int error; // Read serveraddr from config deadbeef->conf_lock (); const char * server = deadbeef->conf_get_str_fast (CONFSTR_PULSE_SERVERADDR, NULL); if (server) { server = strcmp(server, "default") ? server : NULL; } s = pa_simple_new(server, "Deadbeef", PA_STREAM_PLAYBACK, dev, "Music", &ss, &channel_map, attr, &error); deadbeef->conf_unlock (); if (!s) { trace ("pulse_init failed (%d)\n", error); return -1; } return 0; } static int pulse_init(void) { trace ("pulse_init\n"); state = OUTPUT_STATE_STOPPED; pulse_terminate = 0; if (requested_fmt.samplerate != 0) { memcpy (&plugin.fmt, &requested_fmt, sizeof (ddb_waveformat_t)); } if (0 != pulse_set_spec(&plugin.fmt)) { return -1; } pulse_tid = deadbeef->thread_start(pulse_thread, NULL); return 0; } static int pulse_setformat (ddb_waveformat_t *fmt) { memcpy (&requested_fmt, fmt, sizeof (ddb_waveformat_t)); if (!s) { return -1; } if (!memcmp (fmt, &plugin.fmt, sizeof (ddb_waveformat_t))) { trace ("pulse_setformat ignored\n"); return 0; } trace ("pulse_setformat %dbit %s %dch %dHz channelmask=%X\n", fmt->bps, fmt->is_float ? "float" : "int", fmt->channels, fmt->samplerate, fmt->channelmask); int prev_state = state; pulse_stop (); deadbeef->mutex_lock(mutex); pulse_set_spec(fmt); deadbeef->mutex_unlock(mutex); trace ("new format %dbit %s %dch %dHz channelmask=%X\n", plugin.fmt.bps, plugin.fmt.is_float ? "float" : "int", plugin.fmt.channels, plugin.fmt.samplerate, plugin.fmt.channelmask); switch (prev_state) { case OUTPUT_STATE_STOPPED: return pulse_stop (); case OUTPUT_STATE_PLAYING: return pulse_play (); case OUTPUT_STATE_PAUSED: if (0 != pulse_play ()) { return -1; } if (0 != pulse_pause ()) { return -1; } break; } return 0; } static int pulse_free(void) { trace("pulse_free\n"); if (pulse_tid) { pulse_terminate = 1; deadbeef->thread_join(pulse_tid); } pulse_tid = 0; state = OUTPUT_STATE_STOPPED; if (s) { pa_simple_free(s); s = NULL; } return 0; } static int pulse_play(void) { if (!pulse_tid) { if (pulse_init () < 0) { return -1; } } state = OUTPUT_STATE_PLAYING; return 0; } static int pulse_stop(void) { state = OUTPUT_STATE_STOPPED; deadbeef->streamer_reset(1); return 0; } static int pulse_pause(void) { if (state == OUTPUT_STATE_STOPPED) { return -1; } state = OUTPUT_STATE_PAUSED; return 0; } static int pulse_unpause(void) { if (state == OUTPUT_STATE_PAUSED) { state = OUTPUT_STATE_PLAYING; } return 0; } static void pulse_thread(void *context) { #ifdef __linux__ prctl(PR_SET_NAME, "deadbeef-pulse", 0, 0, 0, 0); #endif while (!pulse_terminate) { if (state != OUTPUT_STATE_PLAYING || !deadbeef->streamer_ok_to_read (-1)) { usleep(10000); continue; } int sample_size = plugin.fmt.channels * (plugin.fmt.bps / 8); int bs = buffer_size; int mod = bs % sample_size; if (mod > 0) { bs -= mod; } char buf[bs]; pulse_callback (buf, sizeof (buf)); int error; deadbeef->mutex_lock(mutex); int res = pa_simple_write(s, buf, sizeof (buf), &error); deadbeef->mutex_unlock(mutex); if (res < 0) { fprintf(stderr, "pulse: failed to write buffer\n"); pulse_tid = 0; pulse_free (); break; } } } static void pulse_callback(char *stream, int len) { int bytesread = deadbeef->streamer_read(stream, len); if (bytesread < len) { memset (stream + bytesread, 0, len-bytesread); } } static int pulse_get_state(void) { return state; } static int pulse_plugin_start(void) { mutex = deadbeef->mutex_create(); return 0; } static int pulse_plugin_stop(void) { deadbeef->mutex_free(mutex); return 0; } DB_plugin_t * pulse_load(DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); } static const char settings_dlg[] = "property \"PulseAudio server\" entry " CONFSTR_PULSE_SERVERADDR " default;\n" "property \"Preferred buffer size\" entry " CONFSTR_PULSE_BUFFERSIZE " 8192;\n"; static DB_output_t plugin = { .plugin.api_vmajor = 1, .plugin.api_vminor = 0, .plugin.version_major = 0, .plugin.version_minor = 1, .plugin.type = DB_PLUGIN_OUTPUT, .plugin.id = "pulseaudio", .plugin.name = "PulseAudio output plugin", .plugin.descr = "At the moment of this writing, PulseAudio seems to be very unstable in many (or most) GNU/Linux distributions.\nIf you experience problems - please try switching to ALSA or OSS output.\nIf that doesn't help - please uninstall PulseAudio from your system, and try ALSA or OSS again.\nThanks for understanding", .plugin.copyright = "PulseAudio output plugin for DeaDBeeF Player\n" "Copyright (C) 2011 Jan D. Behrens \n" "Copyright (C) 2010-2012 Alexey Yakovenko \n" "Copyright (C) 2010 Anton Novikov \n" "\n" "This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License\n" "along with this program; if not, write to the Free Software\n" "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n", .plugin.website = "http://deadbeef.sf.net", .plugin.start = pulse_plugin_start, .plugin.stop = pulse_plugin_stop, .plugin.configdialog = settings_dlg, .init = pulse_init, .free = pulse_free, .setformat = pulse_setformat, .play = pulse_play, .stop = pulse_stop, .pause = pulse_pause, .unpause = pulse_unpause, .state = pulse_get_state, };