From b5196b51f6858347bc1af2e243cbc1cd742110ee Mon Sep 17 00:00:00 2001 From: Alexey Yakovenko Date: Fri, 11 Dec 2009 21:40:13 +0100 Subject: alsa code converted into output plugin --- plugins/alsa/alsa.c | 632 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 plugins/alsa/alsa.c (limited to 'plugins/alsa') diff --git a/plugins/alsa/alsa.c b/plugins/alsa/alsa.c new file mode 100644 index 00000000..186a5f9d --- /dev/null +++ b/plugins/alsa/alsa.c @@ -0,0 +1,632 @@ +/* + DeaDBeeF - ultimate music player for GNU/Linux systems with X11 + Copyright (C) 2009 Alexey Yakovenko + + 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 . +*/ +#include +#include +#include +#include +#include +#include "deadbeef.h" + +#define trace(...) { fprintf(stderr, __VA_ARGS__); } +//#define trace(fmt,...) + +static DB_output_t plugin; +DB_functions_t *deadbeef; + +static snd_pcm_t *audio; +static int bufsize = -1; +static int alsa_terminate; +static int alsa_rate = 44100; +static int state; // 0 = stopped, 1 = playing, 2 = pause +static uintptr_t mutex; +static intptr_t alsa_tid; + +static int conf_alsa_resample = 0; +static char conf_alsa_soundcard[100] = "default"; + +static void +palsa_callback (char *stream, int len); + +static void +palsa_thread (void *context); + +static int +palsa_init (void); + +static int +palsa_free (void); + +static int +palsa_change_rate (int rate); + +static int +palsa_play (void); + +static int +palsa_stop (void); + +static int +palsa_isstopped (void); + +static int +palsa_ispaused (void); + +static int +palsa_pause (void); + +static int +palsa_unpause (void); + +static int +palsa_get_rate (void); + +static int +palsa_get_bps (void); + +static int +palsa_get_channels (void); + +static int +palsa_get_endianness (void); + +static void +palsa_enum_soundcards (void (*callback)(const char *name, const char *desc, void*), void *userdata); + +static int +palsa_set_hw_params (int samplerate) { + snd_pcm_hw_params_t *hw_params = NULL; +// int alsa_resample = conf_get_int ("alsa.resample", 0); + int err = 0; + + if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) { + trace ("cannot allocate hardware parameter structure (%s)\n", + snd_strerror (err)); + goto error; + } + + if ((err = snd_pcm_hw_params_any (audio, hw_params)) < 0) { + trace ("cannot initialize hardware parameter structure (%s)\n", + snd_strerror (err)); + goto error; + } + + if ((err = snd_pcm_hw_params_set_access (audio, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + trace ("cannot set access type (%s)\n", + snd_strerror (err)); + goto error; + } + + snd_pcm_format_t fmt; +#if WORDS_BIGENDIAN + fmt = SND_PCM_FORMAT_S16_BE; +#else + fmt = SND_PCM_FORMAT_S16_LE; +#endif + if ((err = snd_pcm_hw_params_set_format (audio, hw_params, fmt)) < 0) { + trace ("cannot set sample format (%s)\n", + snd_strerror (err)); + goto error; + } + + snd_pcm_hw_params_get_format (hw_params, &fmt); + trace ("chosen sample format: %04Xh\n", (int)fmt); + + int val = samplerate; + int ret = 0; + + if ((err = snd_pcm_hw_params_set_rate_resample (audio, hw_params, conf_alsa_resample)) < 0) { + trace ("cannot setup resampling (%s)\n", + snd_strerror (err)); + goto error; + } + + if ((err = snd_pcm_hw_params_set_rate_near (audio, hw_params, &val, &ret)) < 0) { + trace ("cannot set sample rate (%s)\n", + snd_strerror (err)); + goto error; + } + alsa_rate = val; + trace ("chosen samplerate: %d Hz\n", alsa_rate); + + if ((err = snd_pcm_hw_params_set_channels (audio, hw_params, 2)) < 0) { + trace ("cannot set channel count (%s)\n", + snd_strerror (err)); + goto error; + } + + int nchan; + snd_pcm_hw_params_get_channels (hw_params, &nchan); + trace ("alsa channels: %d\n", nchan); + + unsigned int buffer_time = 500000; + int dir; + if ((err = snd_pcm_hw_params_set_buffer_time_near (audio, hw_params, &buffer_time, &dir)) < 0) { + trace ("Unable to set buffer time %i for playback: %s\n", buffer_time, snd_strerror(err)); + goto error; + } + trace ("alsa buffer time: %d ms\n", buffer_time); + snd_pcm_uframes_t size; + if ((err = snd_pcm_hw_params_get_buffer_size (hw_params, &size)) < 0) { + trace ("Unable to get buffer size for playback: %s\n", snd_strerror(err)); + goto error; + } + trace ("alsa buffer size: %d frames\n", size); + bufsize = size; + + if ((err = snd_pcm_hw_params (audio, hw_params)) < 0) { + trace ("cannot set parameters (%s)\n", + snd_strerror (err)); + goto error; + } +error: + if (hw_params) { + snd_pcm_hw_params_free (hw_params); + } + return err; +} + +int +palsa_init (void) { + int err; + + // get and cache conf variables + strcpy (conf_alsa_soundcard, deadbeef->conf_get_str ("alsa_soundcard", "default")); + conf_alsa_resample = deadbeef->conf_get_int ("alsa.resample", 0); + trace ("alsa_soundcard: %s\n", conf_alsa_soundcard); + trace ("alsa.resample: %d\n", conf_alsa_resample); + + snd_pcm_sw_params_t *sw_params = NULL; + state = 0; + //const char *conf_alsa_soundcard = conf_get_str ("alsa_soundcard", "default"); + if ((err = snd_pcm_open (&audio, conf_alsa_soundcard, SND_PCM_STREAM_PLAYBACK, 0))) { + trace ("could not open audio device (%s)\n", + snd_strerror (err)); + return -1; + } + + mutex = deadbeef->mutex_create (); + + if (palsa_set_hw_params (alsa_rate) < 0) { + goto open_error; + } + + if ((err = snd_pcm_sw_params_malloc (&sw_params)) < 0) { + trace ("cannot allocate software parameters structure (%s)\n", + snd_strerror (err)); + goto open_error; + } + if ((err = snd_pcm_sw_params_current (audio, sw_params)) < 0) { + trace ("cannot initialize software parameters structure (%s)\n", + snd_strerror (err)); + goto open_error; + } + + if ((err = snd_pcm_sw_params_set_avail_min (audio, sw_params, bufsize/2)) < 0) { + trace ("cannot set minimum available count (%s)\n", + snd_strerror (err)); + goto open_error; + } + snd_pcm_uframes_t av; + if ((err = snd_pcm_sw_params_get_avail_min (sw_params, &av)) < 0) { + trace ("snd_pcm_sw_params_get_avail_min failed (%s)\n", + snd_strerror (err)); + goto open_error; + } + trace ("alsa period size: %d frames\n", av); + + if ((err = snd_pcm_sw_params_set_start_threshold (audio, sw_params, 0U)) < 0) { + trace ("cannot set start mode (%s)\n", + snd_strerror (err)); + goto open_error; + } + if ((err = snd_pcm_sw_params (audio, sw_params)) < 0) { + trace ("cannot set software parameters (%s)\n", + snd_strerror (err)); + goto open_error; + } + snd_pcm_sw_params_free (sw_params); + sw_params = NULL; + + /* the interface will interrupt the kernel every N frames, and ALSA + will wake up this program very soon after that. + */ + + if ((err = snd_pcm_prepare (audio)) < 0) { + trace ("cannot prepare audio interface for use (%s)\n", + snd_strerror (err)); + goto open_error; + } + + snd_pcm_start (audio); + + alsa_terminate = 0; + alsa_tid = deadbeef->thread_start (palsa_thread, NULL); + + return 0; + +open_error: + if (sw_params) { + snd_pcm_sw_params_free (sw_params); + } + if (audio != NULL) { + palsa_free (); + } + + return -1; +} + +int +palsa_change_rate (int rate) { + if (!audio) { + return 0; + } + if (rate == alsa_rate) { + trace ("palsa_change_rate: same rate (%d), ignored\n", rate); + return rate; + } + trace ("trying to change samplerate to: %d\n", rate); + deadbeef->mutex_lock (mutex); + snd_pcm_drop (audio); + int ret = palsa_set_hw_params (rate); + if (state != 0) { + snd_pcm_start (audio); + } + deadbeef->mutex_unlock (mutex); + if (ret < 0) { + return -1; + } + trace ("chosen samplerate: %d Hz\n", alsa_rate); + return alsa_rate; +} + +int +palsa_free (void) { + trace ("palsa_free\n"); + if (audio && !alsa_terminate) { + alsa_terminate = 1; + deadbeef->thread_join (alsa_tid); + alsa_tid = 0; + snd_pcm_close(audio); + audio = NULL; + deadbeef->mutex_free (mutex); + state = 0; + alsa_terminate = 0; + } + return 0; +} + +static void +palsa_hw_pause (int pause) { + if (!audio) { + return; + } + if (state == 0) { + return; + } + if (pause == 1) { + snd_pcm_drop (audio); + } + else { + snd_pcm_prepare (audio); + snd_pcm_start (audio); + } +} + +int +palsa_play (void) { + int err; + if (state == 0) { + if (!audio) { + if (palsa_init () < 0) { + state = 0; + return -1; + } + } + else { + if ((err = snd_pcm_prepare (audio)) < 0) { + trace ("cannot prepare audio interface for use (%s)\n", + snd_strerror (err)); + return -1; + } + } + } + if (state != 1) { + state = 1; + snd_pcm_start (audio); + } + return 0; +} + + +int +palsa_stop (void) { + if (!audio) { + return 0; + } + state = 0; + if (deadbeef->conf_get_int ("alsa.freeonstop", 0)) { + palsa_free (); + } + else { + deadbeef->mutex_lock (mutex); + snd_pcm_drop (audio); + deadbeef->mutex_unlock (mutex); + } + deadbeef->streamer_reset (1); + return 0; +} + +int +palsa_isstopped (void) { + return (state == 0); +} + +int +palsa_ispaused (void) { + // return pause state + return (state == 2); +} + +int +palsa_pause (void) { + if (state == 0 || !audio) { + return -1; + } + // set pause state + palsa_hw_pause (1); + state = 2; + return 0; +} + +int +palsa_unpause (void) { + // unset pause state + if (state == 2) { + state = 1; + palsa_hw_pause (0); + } + return 0; +} + +int +palsa_get_rate (void) { + return alsa_rate; +} + +int +palsa_get_bps (void) { + return 16; +} + +int +palsa_get_channels (void) { + return 2; +} + +static int +palsa_get_endianness (void) { +#if WORDS_BIGENDIAN + return 1; +#else + return 0; +#endif +} + +static void +palsa_thread (void *context) { + prctl (PR_SET_NAME, "deadbeef-alsa", 0, 0, 0, 0); + int err; + for (;;) { + if (alsa_terminate) { + break; + } + if (state != 1) { + usleep (10000); + continue; + } + + deadbeef->mutex_lock (mutex); + /* wait till the interface is ready for data, or 1 second + has elapsed. + */ + if ((err = snd_pcm_wait (audio, 500)) < 0 && state == 1) { + if (err == -ESTRPIPE) { + trace ("alsa: trying to recover from suspend...\n"); + deadbeef->sendmessage (M_REINIT_SOUND, 0, 0, 0); + deadbeef->mutex_unlock (mutex); + break; + } + else { + trace ("alsa: trying to recover from xrun...\n"); + snd_pcm_prepare (audio); + deadbeef->mutex_unlock (mutex); + continue; + } + } + + /* find out how much space is available for playback data */ + + snd_pcm_sframes_t frames_to_deliver; + if ((frames_to_deliver = snd_pcm_avail_update (audio)) < 0) { + if (frames_to_deliver == -EPIPE) { + deadbeef->mutex_unlock (mutex); + trace ("an xrun occured\n"); + continue; + } else { + deadbeef->mutex_unlock (mutex); + trace ("unknown ALSA avail update return value (%d)\n", + (int)frames_to_deliver); + continue; + } + } + + /* deliver the data */ + // FIXME: under some conditions, frames_to_deliver may become huge + // like 20M. this case is not handled here. + char buf[frames_to_deliver*4]; + palsa_callback (buf, frames_to_deliver*4); + if ((err = snd_pcm_writei (audio, buf, frames_to_deliver)) < 0) { + trace ("write %d frames failed (%s)\n", frames_to_deliver, snd_strerror (err)); + snd_pcm_prepare (audio); + snd_pcm_start (audio); + } + deadbeef->mutex_unlock (mutex); + usleep (1000); // this must be here to prevent mutex deadlock + } +} + +static void +palsa_callback (char *stream, int len) { + if (!deadbeef->streamer_ok_to_read (len)) { + memset (stream, 0, len); + return; + } + int bytesread = deadbeef->streamer_read (stream, len); + +// FIXME: move volume control to streamer_read for copy optimization +#if 0 + int16_t vol[4]; + vol[0] = volume_get_amp () * 255; // that will be extra 8 bits + // pack 4 times + vol[1] = vol[2] = vol[3] = vol[0]; + + // apply volume with mmx + __asm__ volatile( + " mov %0, %%ecx\n\t" + " shr $4, %%ecx\n\t" + " mov %1, %%eax\n\t" + " movq %2, %mm1\n\t" + "1:\n\t" + " movq [%%eax], %mm0\n\t" + " movq %mm0, %mm2\n\t" + " movq %mm0, %mm3\n\t" + " pmullw %mm1, %mm2\n\t" + " pmulhw %mm1, %mm3\n\t" + " psrlw $8, %mm2\n\t" // discard lowest 8 bits + " psllw $8, %mm3\n\t" // shift left 8 lsbs of hiwords + " por %mm3, %mm2\n\t" // OR them together + " movq %mm3, [%%eax]\n\t" // load back to memory + " add $8, %%eax\n\t" + " dec %%ecx\n\t" + " jnz 1b\n\t" + : + : "r"(len), "r"(stream), "r"(vol) + : "%ecx", "%eax" + ); + +#else + int16_t ivolume = deadbeef->volume_get_amp () * 1000; + for (int i = 0; i < bytesread/2; i++) { + ((int16_t*)stream)[i] = (int16_t)(((int32_t)(((int16_t*)stream)[i])) * ivolume / 1000); + } +#endif + if (bytesread < len) { + memset (stream + bytesread, 0, len-bytesread); + } +} + +static int +palsa_configchanged (DB_event_t *ev, uintptr_t data) { + int alsa_resample = deadbeef->conf_get_int ("alsa.resample", 0); + const char *alsa_soundcard = deadbeef->conf_get_str ("alsa_soundcard", "default"); + if (alsa_resample != conf_alsa_resample + || strcmp (alsa_soundcard, conf_alsa_soundcard)) { + deadbeef->sendmessage (M_REINIT_SOUND, 0, 0, 0); + } +} + +// derived from alsa-utils/aplay.c +void +palsa_enum_soundcards (void (*callback)(const char *name, const char *desc, void *), void *userdata) { + void **hints, **n; + char *name, *descr, *descr1, *io; + const char *filter = "Output"; + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return; + n = hints; + while (*n != NULL) { + name = snd_device_name_get_hint(*n, "NAME"); + descr = snd_device_name_get_hint(*n, "DESC"); + io = snd_device_name_get_hint(*n, "IOID"); + if (io == NULL || !strcmp(io, filter)) { + if (name && descr && callback) { + callback (name, descr, userdata); + } + } + if (name != NULL) + free(name); + if (descr != NULL) + free(descr); + if (io != NULL) + free(io); + n++; + } + snd_device_name_free_hint(hints); +} + +int +palsa_get_state (void) { + return state; +} + +int +alsa_start (void) { + deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_CONFIGCHANGED, DB_CALLBACK (palsa_configchanged), 0); + return 0; +} + +int +alsa_stop (void) { + deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_CONFIGCHANGED, DB_CALLBACK (palsa_configchanged), 0); + return 0; +} + +DB_plugin_t * +alsa_load (DB_functions_t *api) { + deadbeef = api; + return DB_PLUGIN (&plugin); +} + +// define plugin interface +static DB_output_t plugin = { + DB_PLUGIN_SET_API_VERSION + .plugin.version_major = 0, + .plugin.version_minor = 1, + .plugin.nostop = 1, + .plugin.type = DB_PLUGIN_OUTPUT, + .plugin.name = "alsa output plugin", + .plugin.descr = "plays sound through linux standard alsa library", + .plugin.author = "Alexey Yakovenko", + .plugin.email = "waker@users.sourceforge.net", + .plugin.website = "http://deadbeef.sf.net", + .plugin.start = alsa_start, + .plugin.stop = alsa_stop, + .init = palsa_init, + .free = palsa_free, + .change_rate = palsa_change_rate, + .play = palsa_play, + .stop = palsa_stop, + .pause = palsa_pause, + .unpause = palsa_unpause, + .state = palsa_get_state, + .samplerate = palsa_get_rate, + .bitspersample = palsa_get_bps, + .channels = palsa_get_channels, + .endianness = palsa_get_endianness, + .enum_soundcards = palsa_enum_soundcards, +}; -- cgit v1.2.3