Mercurial > audlegacy-plugins
diff src/Output/alsa/audio.c @ 0:13389e613d67 trunk
[svn] - initial import of audacious-plugins tree (lots to do)
author | nenolod |
---|---|
date | Mon, 18 Sep 2006 01:11:49 -0700 |
parents | |
children | 6303e3a8a6b8 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Output/alsa/audio.c Mon Sep 18 01:11:49 2006 -0700 @@ -0,0 +1,1196 @@ +/* XMMS - ALSA output plugin + * Copyright (C) 2001-2003 Matthieu Sozeau <mattam@altern.org> + * Copyright (C) 1998-2003 Peter Alm, Mikael Alm, Olle Hallnas, + * Thomas Nilsson and 4Front Technologies + * Copyright (C) 1999-2005 Haavard Kvaalen + * Copyright (C) 2005 Takashi Iwai + * + * 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. + */ + +/* + * CHANGES + * + * 2005.01.05 Takashi Iwai <tiwai@suse.de> + * Impelemented the multi-threaded mode with an audio-thread. + * Many fixes and cleanups. + */ + + +#include "alsa.h" +#include <ctype.h> +#include <libaudacious/xconvert.h> + +static snd_pcm_t *alsa_pcm; +static snd_output_t *logs; + + +/* Number of bytes that we have received from the input plugin */ +static guint64 alsa_total_written; +/* Number of bytes that we have sent to the sound card */ +static guint64 alsa_hw_written; +static guint64 output_time_offset; + +/* device buffer/period sizes in bytes */ +static int hw_buffer_size, hw_period_size; /* in output bytes */ +static int hw_buffer_size_in, hw_period_size_in; /* in input bytes */ + +/* Set/Get volume */ +static snd_mixer_elem_t *pcm_element; +static snd_mixer_t *mixer; + +static gboolean going, paused, mixer_start = TRUE; +static gboolean prebuffer, remove_prebuffer; + +static gboolean alsa_can_pause; + +/* for audio thread */ +static GThread *audio_thread; /* audio loop thread */ +static int thread_buffer_size; /* size of intermediate buffer in bytes */ +static char *thread_buffer; /* audio intermediate buffer */ +static int rd_index, wr_index; /* current read/write position in int-buffer */ +static gboolean pause_request; /* pause status currently requested */ +static int flush_request; /* flush status (time) currently requested */ +static int prebuffer_size; +GMutex *alsa_mutex; + +static guint mixer_timeout; + +struct snd_format { + unsigned int rate; + unsigned int channels; + snd_pcm_format_t format; + AFormat xmms_format; + int sample_bits; + int bps; +}; + +static struct snd_format *inputf = NULL; +static struct snd_format *effectf = NULL; +static struct snd_format *outputf = NULL; + +static int alsa_setup(struct snd_format *f); +static void alsa_write_audio(char *data, int length); +static void alsa_cleanup_mixer(void); +static int get_thread_buffer_filled(void); + +static struct snd_format * snd_format_from_xmms(AFormat fmt, int rate, int channels); + +static struct xmms_convert_buffers *convertb; + +static convert_func_t alsa_convert_func; +static convert_channel_func_t alsa_stereo_convert_func; +static convert_freq_func_t alsa_frequency_convert_func; + +static const struct { + AFormat xmms; + snd_pcm_format_t alsa; +} format_table[] = +{{FMT_S16_LE, SND_PCM_FORMAT_S16_LE}, + {FMT_S16_BE, SND_PCM_FORMAT_S16_BE}, + {FMT_S16_NE, SND_PCM_FORMAT_S16}, + {FMT_U16_LE, SND_PCM_FORMAT_U16_LE}, + {FMT_U16_BE, SND_PCM_FORMAT_U16_BE}, + {FMT_U16_NE, SND_PCM_FORMAT_U16}, + {FMT_U8, SND_PCM_FORMAT_U8}, + {FMT_S8, SND_PCM_FORMAT_S8}, +}; + + +static void debug(char *str, ...) G_GNUC_PRINTF(1, 2); + +static void debug(char *str, ...) +{ + va_list args; + + if (alsa_cfg.debug) + { + va_start(args, str); + g_logv(NULL, G_LOG_LEVEL_MESSAGE, str, args); + va_end(args); + } +} + +int alsa_playing(void) +{ + if (!going || paused || alsa_pcm == NULL) + return FALSE; + + return snd_pcm_state(alsa_pcm) == SND_PCM_STATE_RUNNING; +} + +static int xrun_recover(void) +{ + if (alsa_cfg.debug) + { + snd_pcm_status_t *alsa_status; + snd_pcm_status_alloca(&alsa_status); + if (snd_pcm_status(alsa_pcm, alsa_status) < 0) + g_warning("xrun_recover(): snd_pcm_status() failed"); + else + { + printf("Status:\n"); + snd_pcm_status_dump(alsa_status, logs); + } + } + return snd_pcm_prepare(alsa_pcm); +} + +static int suspend_recover(void) +{ + int err; + + while ((err = snd_pcm_resume(alsa_pcm)) == -EAGAIN) + /* wait until suspend flag is released */ + xmms_usleep(1000000); + if (err < 0) + { + g_warning("alsa_handle_error(): " + "snd_pcm_resume() failed."); + return snd_pcm_prepare(alsa_pcm); + } + return err; +} + +/* handle generic errors */ +static int alsa_handle_error(int err) +{ + switch (err) + { + case -EPIPE: + return xrun_recover(); + case -ESTRPIPE: + return suspend_recover(); + } + + return err; +} + +/* update and get the available space on h/w buffer (in frames) */ +static snd_pcm_sframes_t alsa_get_avail(void) +{ + snd_pcm_sframes_t ret; + + if (alsa_pcm == NULL) + return 0; + + while ((ret = snd_pcm_avail_update(alsa_pcm)) < 0) + { + ret = alsa_handle_error(ret); + if (ret < 0) + { + g_warning("alsa_get_avail(): snd_pcm_avail_update() failed: %s", + snd_strerror(-ret)); + return 0; + } + } + return ret; +} + +/* get the free space on buffer */ +int alsa_free(void) +{ + if (remove_prebuffer && prebuffer) + { + prebuffer = FALSE; + remove_prebuffer = FALSE; + } + if (prebuffer) + remove_prebuffer = TRUE; + + return thread_buffer_size - get_thread_buffer_filled() - 1; +} + +/* do pause operation */ +static void alsa_do_pause(gboolean p) +{ + if (paused == p) + return; + + if (alsa_pcm) + { + if (alsa_can_pause) + snd_pcm_pause(alsa_pcm, p); + else if (p) + { + snd_pcm_drop(alsa_pcm); + snd_pcm_prepare(alsa_pcm); + } + } + paused = p; +} + +void alsa_pause(short p) +{ + debug("alsa_pause"); + pause_request = p; +} + +/* close PCM and release associated resources */ +static void alsa_close_pcm(void) +{ + if (alsa_pcm) + { + int err; + snd_pcm_drop(alsa_pcm); + if ((err = snd_pcm_close(alsa_pcm)) < 0) + g_warning("alsa_pcm_close() failed: %s", + snd_strerror(-err)); + alsa_pcm = NULL; + } +} + +void alsa_close(void) +{ + if (!going) + return; + + debug("Closing device"); + + going = 0; + + g_thread_join(audio_thread); + + g_mutex_lock(alsa_mutex); /* alsa_loop locks alsa_mutex! */ + + alsa_cleanup_mixer(); + + xmms_convert_buffers_destroy(convertb); + convertb = NULL; + g_free(inputf); + inputf = NULL; + g_free(effectf); + effectf = NULL; + g_free(outputf); + outputf = NULL; + + alsa_save_config(); + + if (alsa_cfg.debug) + snd_output_close(logs); + debug("Device closed"); + + g_mutex_unlock(alsa_mutex); +} + +/* reopen ALSA PCM */ +static int alsa_reopen(struct snd_format *f) +{ + /* remember the current position */ + output_time_offset += (alsa_hw_written * 1000) / outputf->bps; + alsa_hw_written = 0; + + alsa_close_pcm(); + + return alsa_setup(f); +} + +/* do flush (drop) operation */ +static void alsa_do_flush(int time) +{ + if (alsa_pcm) + { + snd_pcm_drop(alsa_pcm); + snd_pcm_prepare(alsa_pcm); + } + /* correct the offset */ + output_time_offset = time; + alsa_total_written = (guint64) time * inputf->bps / 1000; + rd_index = wr_index = alsa_hw_written = 0; +} + +void alsa_flush(int time) +{ + flush_request = time; + while (flush_request != -1) + xmms_usleep(10000); +} + +static void parse_mixer_name(char *str, char **name, int *index) +{ + char *end; + + while (isspace(*str)) + str++; + + if ((end = strchr(str, ',')) != NULL) + { + *name = g_strndup(str, end - str); + end++; + *index = atoi(end); + } + else + { + *name = g_strdup(str); + *index = 0; + } +} + +int alsa_get_mixer(snd_mixer_t **mixer, int card) +{ + char *dev; + int err; + + debug("alsa_get_mixer"); + + dev = g_strdup_printf("hw:%i", card); + + if ((err = snd_mixer_open(mixer, 0)) < 0) + { + g_warning("alsa_get_mixer(): Failed to open empty mixer: %s", + snd_strerror(-err)); + mixer = NULL; + return -1; + } + if ((err = snd_mixer_attach(*mixer, dev)) < 0) + { + g_warning("alsa_get_mixer(): Attaching to mixer %s failed: %s", + dev, snd_strerror(-err)); + return -1; + } + if ((err = snd_mixer_selem_register(*mixer, NULL, NULL)) < 0) + { + g_warning("alsa_get_mixer(): Failed to register mixer: %s", + snd_strerror(-err)); + return -1; + } + if ((err = snd_mixer_load(*mixer)) < 0) + { + g_warning("alsa_get_mixer(): Failed to load mixer: %s", + snd_strerror(-err)); + return -1; + } + + g_free(dev); + + return (*mixer != NULL); +} + + +static snd_mixer_elem_t* alsa_get_mixer_elem(snd_mixer_t *mixer, char *name, int index) +{ + snd_mixer_selem_id_t *selem_id; + snd_mixer_elem_t* elem; + snd_mixer_selem_id_alloca(&selem_id); + + if (index != -1) + snd_mixer_selem_id_set_index(selem_id, index); + if (name != NULL) + snd_mixer_selem_id_set_name(selem_id, name); + + elem = snd_mixer_find_selem(mixer, selem_id); + + return elem; +} + +static int alsa_setup_mixer(void) +{ + char *name; + long int a, b; + long alsa_min_vol, alsa_max_vol; + int err, index; + + debug("alsa_setup_mixer"); + + if ((err = alsa_get_mixer(&mixer, alsa_cfg.mixer_card)) < 0) + return err; + + parse_mixer_name(alsa_cfg.mixer_device, &name, &index); + + pcm_element = alsa_get_mixer_elem(mixer, name, index); + + g_free(name); + + if (!pcm_element) + { + g_warning("alsa_setup_mixer(): Failed to find mixer element: %s", + alsa_cfg.mixer_device); + return -1; + } + + /* + * Work around a bug in alsa-lib up to 1.0.0rc2 where the + * new range don't take effect until the volume is changed. + * This hack should be removed once we depend on Alsa 1.0.0. + */ + snd_mixer_selem_get_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_LEFT, &a); + snd_mixer_selem_get_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_RIGHT, &b); + + snd_mixer_selem_get_playback_volume_range(pcm_element, + &alsa_min_vol, &alsa_max_vol); + snd_mixer_selem_set_playback_volume_range(pcm_element, 0, 100); + + if (alsa_max_vol == 0) + { + pcm_element = NULL; + return -1; + } + + if (!alsa_cfg.soft_volume) + alsa_set_volume(a * 100 / alsa_max_vol, b * 100 / alsa_max_vol); + + debug("alsa_setup_mixer: end"); + + return 0; +} + +static int alsa_mixer_timeout(void *data) +{ + if (mixer) + { + snd_mixer_close(mixer); + mixer = NULL; + pcm_element = NULL; + } + mixer_timeout = 0; + mixer_start = TRUE; + + g_message("alsa mixer timed out"); + return FALSE; +} + +static void alsa_cleanup_mixer(void) +{ + pcm_element = NULL; + if (mixer) + { + snd_mixer_close(mixer); + mixer = NULL; + } +} + +void alsa_get_volume(int *l, int *r) +{ + long ll = *l, lr = *r; + + if (mixer_start) + { + alsa_setup_mixer(); + mixer_start = FALSE; + } + + if (alsa_cfg.soft_volume) + { + *l = alsa_cfg.vol.left; + *r = alsa_cfg.vol.right; + } + + if (!pcm_element) + return; + + snd_mixer_handle_events(mixer); + + if (!alsa_cfg.soft_volume) + { + snd_mixer_selem_get_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_LEFT, + &ll); + snd_mixer_selem_get_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_RIGHT, + &lr); + *l = ll; + *r = lr; + } + if (mixer_timeout) + gtk_timeout_remove(mixer_timeout); + mixer_timeout = gtk_timeout_add(5000, alsa_mixer_timeout, NULL); +} + + +void alsa_set_volume(int l, int r) +{ + if (alsa_cfg.soft_volume) + { + alsa_cfg.vol.left = l; + alsa_cfg.vol.right = r; + return; + } + + if (!pcm_element) + return; + + snd_mixer_selem_set_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_LEFT, l); + snd_mixer_selem_set_playback_volume(pcm_element, + SND_MIXER_SCHN_FRONT_RIGHT, r); +} + + +/* + * audio stuff + */ + +/* return the size of audio data filled in the audio thread buffer */ +static int get_thread_buffer_filled(void) +{ + if (wr_index >= rd_index) + return wr_index - rd_index; + return thread_buffer_size - (rd_index - wr_index); +} + +int alsa_get_output_time(void) +{ + snd_pcm_sframes_t delay; + guint64 bytes = alsa_hw_written; + + if (!going || alsa_pcm == NULL) + return 0; + + if (!snd_pcm_delay(alsa_pcm, &delay)) + { + unsigned int d = snd_pcm_frames_to_bytes(alsa_pcm, delay); + if (bytes < d) + bytes = 0; + else + bytes -= d; + } + return output_time_offset + (bytes * 1000) / outputf->bps; +} + +int alsa_get_written_time(void) +{ + if (!going) + return 0; + return (alsa_total_written * 1000) / inputf->bps; +} + +#define STEREO_ADJUST(type, type2, endian) \ +do { \ + type *ptr = data; \ + for (i = 0; i < length; i += 4) \ + { \ + *ptr = type2##_TO_##endian(type2##_FROM_## endian(*ptr) * \ + alsa_cfg.vol.left / 100); \ + ptr++; \ + *ptr = type2##_TO_##endian(type2##_FROM_##endian(*ptr) * \ + alsa_cfg.vol.right / 100); \ + ptr++; \ + } \ +} while (0) + +#define MONO_ADJUST(type, type2, endian) \ +do { \ + type *ptr = data; \ + for (i = 0; i < length; i += 2) \ + { \ + *ptr = type2##_TO_##endian(type2##_FROM_## endian(*ptr) * \ + vol / 100); \ + ptr++; \ + } \ +} while (0) + +#define VOLUME_ADJUST(type, type2, endian) \ +do { \ + if (channels == 2) \ + STEREO_ADJUST(type, type2, endian); \ + else \ + MONO_ADJUST(type, type2, endian); \ +} while (0) + +#define STEREO_ADJUST8(type) \ +do { \ + type *ptr = data; \ + for (i = 0; i < length; i += 2) \ + { \ + *ptr = *ptr * alsa_cfg.vol.left / 100; \ + ptr++; \ + *ptr = *ptr * alsa_cfg.vol.right / 100; \ + ptr++; \ + } \ +} while (0) + +#define MONO_ADJUST8(type) \ +do { \ + type *ptr = data; \ + for (i = 0; i < length; i++) \ + { \ + *ptr = *ptr * vol / 100; \ + ptr++; \ + } \ +} while (0) + +#define VOLUME_ADJUST8(type) \ +do { \ + if (channels == 2) \ + STEREO_ADJUST8(type); \ + else \ + MONO_ADJUST8(type); \ +} while (0) + + +static void volume_adjust(void* data, int length, AFormat fmt, int channels) +{ + int i, vol; + + if ((alsa_cfg.vol.left == 100 && alsa_cfg.vol.right == 100) || + (channels == 1 && + (alsa_cfg.vol.left == 100 || alsa_cfg.vol.right == 100))) + return; + + vol = MAX(alsa_cfg.vol.left, alsa_cfg.vol.right); + + switch (fmt) + { + case FMT_S16_LE: + VOLUME_ADJUST(gint16, GINT16, LE); + break; + case FMT_U16_LE: + VOLUME_ADJUST(guint16, GUINT16, LE); + break; + case FMT_S16_BE: + VOLUME_ADJUST(gint16, GINT16, BE); + break; + case FMT_U16_BE: + VOLUME_ADJUST(guint16, GUINT16, BE); + break; + case FMT_S8: + VOLUME_ADJUST8(gint8); + break; + case FMT_U8: + VOLUME_ADJUST8(guint8); + break; + default: + g_warning("volue_adjust(): unhandled format: %d", fmt); + break; + } +} + + +/* transfer data to audio h/w; length is given in bytes + * + * data can be modified via effect plugin, rate conversion or + * software volume before passed to audio h/w + */ +static void alsa_do_write(gpointer data, int length) +{ + EffectPlugin *ep = NULL; + int new_freq; + int new_chn; + AFormat f; + + if (paused) + return; + + new_freq = inputf->rate; + new_chn = inputf->channels; + f = inputf->xmms_format; + + if (effects_enabled() && (ep = get_current_effect_plugin()) && + ep->query_format) + ep->query_format(&f, &new_freq, &new_chn); + + if (f != effectf->xmms_format || (unsigned int)new_freq != effectf->rate || + (unsigned int)new_chn != effectf->channels) + { + debug("Changing audio format for effect plugin"); + g_free(effectf); + effectf = snd_format_from_xmms(f, new_freq, new_chn); + if (alsa_reopen(effectf) < 0) + { + /* fatal error... */ + alsa_close(); + return; + } + } + + if (ep) + length = ep->mod_samples(&data, length, + inputf->xmms_format, + inputf->rate, + inputf->channels); + + if (alsa_convert_func != NULL) + length = alsa_convert_func(convertb, &data, length); + if (alsa_stereo_convert_func != NULL) + length = alsa_stereo_convert_func(convertb, &data, length); + if (alsa_frequency_convert_func != NULL) + length = alsa_frequency_convert_func(convertb, &data, length, + effectf->rate, + outputf->rate); + + if (alsa_cfg.soft_volume) + volume_adjust(data, length, outputf->xmms_format, outputf->channels); + + alsa_write_audio(data, length); +} + +/* write callback */ +void alsa_write(gpointer data, int length) +{ + int cnt; + char *src = (char *)data; + + remove_prebuffer = FALSE; + + alsa_total_written += length; + while (length > 0) + { + int wr; + cnt = MIN(length, thread_buffer_size - wr_index); + memcpy(thread_buffer + wr_index, src, cnt); + wr = (wr_index + cnt) % thread_buffer_size; + wr_index = wr; + length -= cnt; + src += cnt; + } +} + +/* transfer data to audio h/w via normal write */ +static void alsa_write_audio(char *data, int length) +{ + snd_pcm_sframes_t written_frames; + + while (length > 0) + { + int frames = snd_pcm_bytes_to_frames(alsa_pcm, length); + written_frames = snd_pcm_writei(alsa_pcm, data, frames); + + if (written_frames > 0) + { + int written = snd_pcm_frames_to_bytes(alsa_pcm, + written_frames); + length -= written; + data += written; + alsa_hw_written += written; + } + else + { + int err = alsa_handle_error((int)written_frames); + if (err < 0) + { + g_warning("alsa_write_audio(): write error: %s", + snd_strerror(-err)); + break; + } + } + } +} + +/* transfer audio data from thread buffer to h/w */ +static void alsa_write_out_thread_data(void) +{ + gint length, cnt, avail; + + length = MIN(hw_period_size_in, get_thread_buffer_filled()); + avail = snd_pcm_frames_to_bytes(alsa_pcm, alsa_get_avail()); + length = MIN(length, avail); + while (length > 0) + { + int rd; + cnt = MIN(length, thread_buffer_size - rd_index); + alsa_do_write(thread_buffer + rd_index, cnt); + rd = (rd_index + cnt) % thread_buffer_size; + rd_index = rd; + length -= cnt; + } +} + +/* audio thread loop */ +/* FIXME: proper lock? */ +static void *alsa_loop(void *arg) +{ + int npfds = snd_pcm_poll_descriptors_count(alsa_pcm); + struct pollfd *pfds; + unsigned short *revents; + + g_mutex_lock(alsa_mutex); + + if (npfds <= 0) + goto _error; + pfds = alloca(sizeof(*pfds) * npfds); + revents = alloca(sizeof(*revents) * npfds); + while (going && alsa_pcm) + { + if (get_thread_buffer_filled() > prebuffer_size) + prebuffer = FALSE; + if (!paused && !prebuffer && + get_thread_buffer_filled() > hw_period_size_in) + { + snd_pcm_poll_descriptors(alsa_pcm, pfds, npfds); + if (poll(pfds, npfds, 10) > 0) + { + /* + * need to check revents. poll() with + * dmix returns a postive value even + * if no data is available + */ + int i; + snd_pcm_poll_descriptors_revents(alsa_pcm, pfds, + npfds, revents); + for (i = 0; i < npfds; i++) + if (revents[i] & POLLOUT) + { + alsa_write_out_thread_data(); + break; + } + } + } + else + xmms_usleep(10000); + + if (pause_request != paused) + alsa_do_pause(pause_request); + + if (flush_request != -1) + { + alsa_do_flush(flush_request); + flush_request = -1; + prebuffer = TRUE; + } + } + + _error: + g_mutex_unlock(alsa_mutex); + alsa_close_pcm(); + g_free(thread_buffer); + thread_buffer = NULL; + g_thread_exit(NULL); + return(NULL); +} + +/* open callback */ +int alsa_open(AFormat fmt, int rate, int nch) +{ + debug("Opening device"); + inputf = snd_format_from_xmms(fmt, rate, nch); + effectf = snd_format_from_xmms(fmt, rate, nch); + + if (alsa_cfg.debug) + snd_output_stdio_attach(&logs, stdout, 0); + + if (alsa_setup(inputf) < 0) + { + alsa_close(); + return 0; + } + + g_mutex_lock(alsa_mutex); + + if (!mixer) + alsa_setup_mixer(); + + convertb = xmms_convert_buffers_new(); + + output_time_offset = 0; + alsa_total_written = alsa_hw_written = 0; + going = TRUE; + paused = FALSE; + prebuffer = TRUE; + remove_prebuffer = FALSE; + + thread_buffer_size = (guint64)cfg.output_buffer_size * inputf->bps / 1000; + if (thread_buffer_size < hw_buffer_size) + thread_buffer_size = hw_buffer_size * 2; + if (thread_buffer_size < 8192) + thread_buffer_size = 8192; + prebuffer_size = thread_buffer_size / 2; + if (prebuffer_size < 8192) + prebuffer_size = 8192; + thread_buffer_size += hw_buffer_size; + thread_buffer_size -= thread_buffer_size % hw_period_size; + thread_buffer = g_malloc0(thread_buffer_size); + wr_index = rd_index = 0; + pause_request = FALSE; + flush_request = -1; + + g_mutex_unlock(alsa_mutex); + + audio_thread = g_thread_create((GThreadFunc)alsa_loop, NULL, TRUE, NULL); + return 1; +} + +static struct snd_format * snd_format_from_xmms(AFormat fmt, int rate, int channels) +{ + struct snd_format *f = g_malloc(sizeof(struct snd_format)); + size_t i; + + f->xmms_format = fmt; + f->format = SND_PCM_FORMAT_UNKNOWN; + + for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++) + if (format_table[i].xmms == fmt) + { + f->format = format_table[i].alsa; + break; + } + + /* Get rid of _NE */ + for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++) + if (format_table[i].alsa == f->format) + { + f->xmms_format = format_table[i].xmms; + break; + } + + + f->rate = rate; + f->channels = channels; + f->sample_bits = snd_pcm_format_physical_width(f->format); + f->bps = (rate * f->sample_bits * channels) >> 3; + + return f; +} + +static int format_from_alsa(snd_pcm_format_t fmt) +{ + size_t i; + for (i = 0; i < sizeof(format_table) / sizeof(format_table[0]); i++) + if (format_table[i].alsa == fmt) + return format_table[i].xmms; + g_warning("Unsupported format: %s", snd_pcm_format_name(fmt)); + return -1; +} + +static int alsa_setup(struct snd_format *f) +{ + int err; + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; + uint alsa_buffer_time; + unsigned int alsa_period_time; + snd_pcm_uframes_t alsa_buffer_size, alsa_period_size; + + debug("alsa_setup"); + + alsa_convert_func = NULL; + alsa_stereo_convert_func = NULL; + alsa_frequency_convert_func = NULL; + + g_free(outputf); + outputf = snd_format_from_xmms(f->xmms_format, f->rate, f->channels); + + debug("Opening device: %s", alsa_cfg.pcm_device); + /* FIXME: Can snd_pcm_open() return EAGAIN? */ + if ((err = snd_pcm_open(&alsa_pcm, alsa_cfg.pcm_device, + SND_PCM_STREAM_PLAYBACK, + SND_PCM_NONBLOCK)) < 0) + { + g_warning("alsa_setup(): Failed to open pcm device (%s): %s", + alsa_cfg.pcm_device, snd_strerror(-err)); + alsa_pcm = NULL; + g_free(outputf); + outputf = NULL; + return -1; + } + + /* doesn't care about non-blocking */ + /* snd_pcm_nonblock(alsa_pcm, 0); */ + + if (alsa_cfg.debug) + { + snd_pcm_info_t *info; + int alsa_card, alsa_device, alsa_subdevice; + + snd_pcm_info_alloca(&info); + snd_pcm_info(alsa_pcm, info); + alsa_card = snd_pcm_info_get_card(info); + alsa_device = snd_pcm_info_get_device(info); + alsa_subdevice = snd_pcm_info_get_subdevice(info); + printf("Card %i, Device %i, Subdevice %i\n", + alsa_card, alsa_device, alsa_subdevice); + } + + snd_pcm_hw_params_alloca(&hwparams); + + if ((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) + { + g_warning("alsa_setup(): No configuration available for " + "playback: %s", snd_strerror(-err)); + return -1; + } + + if ((err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) + { + g_warning("alsa_setup(): Cannot set direct write mode: %s", + snd_strerror(-err)); + return -1; + } + + if ((err = snd_pcm_hw_params_set_format(alsa_pcm, hwparams, outputf->format)) < 0) + { + /* + * Try if one of these format work (one of them should work + * on almost all soundcards) + */ + snd_pcm_format_t formats[] = {SND_PCM_FORMAT_S16_LE, + SND_PCM_FORMAT_S16_BE, + SND_PCM_FORMAT_U8}; + size_t i; + + for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) + { + if (snd_pcm_hw_params_set_format(alsa_pcm, hwparams, + formats[i]) == 0) + { + outputf->format = formats[i]; + break; + } + } + if (outputf->format != f->format) + { + outputf->xmms_format = + format_from_alsa(outputf->format); + debug("Converting format from %d to %d", + f->xmms_format, outputf->xmms_format); + alsa_convert_func = + xmms_convert_get_func(outputf->xmms_format, + f->xmms_format); + if (alsa_convert_func == NULL) + return -1; + } + else + { + g_warning("alsa_setup(): Sample format not " + "available for playback: %s", + snd_strerror(-err)); + return -1; + } + } + + snd_pcm_hw_params_set_channels_near(alsa_pcm, hwparams, &outputf->channels); + if (outputf->channels != f->channels) + { + debug("Converting channels from %d to %d", + f->channels, outputf->channels); + alsa_stereo_convert_func = + xmms_convert_get_channel_func(outputf->xmms_format, + outputf->channels, + f->channels); + if (alsa_stereo_convert_func == NULL) + return -1; + } + + snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &outputf->rate, 0); + if (outputf->rate == 0) + { + g_warning("alsa_setup(): No usable samplerate available."); + return -1; + } + if (outputf->rate != f->rate) + { + debug("Converting samplerate from %d to %d", + f->rate, outputf->rate); + alsa_frequency_convert_func = + xmms_convert_get_frequency_func(outputf->xmms_format, + outputf->channels); + if (alsa_frequency_convert_func == NULL) + return -1; + } + + outputf->sample_bits = snd_pcm_format_physical_width(outputf->format); + outputf->bps = (outputf->rate * outputf->sample_bits * outputf->channels) >> 3; + + alsa_buffer_time = alsa_cfg.buffer_time * 1000; + if ((err = snd_pcm_hw_params_set_buffer_time_near(alsa_pcm, hwparams, + &alsa_buffer_time, 0)) < 0) + { + g_warning("alsa_setup(): Set buffer time failed: %s.", + snd_strerror(-err)); + return -1; + } + + alsa_period_time = alsa_cfg.period_time * 1000; + if ((err = snd_pcm_hw_params_set_period_time_near(alsa_pcm, hwparams, + &alsa_period_time, 0)) < 0) + { + g_warning("alsa_setup(): Set period time failed: %s.", + snd_strerror(-err)); + return -1; + } + + if (snd_pcm_hw_params(alsa_pcm, hwparams) < 0) + { + if (alsa_cfg.debug) + snd_pcm_hw_params_dump(hwparams, logs); + g_warning("alsa_setup(): Unable to install hw params"); + return -1; + } + + if ((err = snd_pcm_hw_params_get_buffer_size(hwparams, &alsa_buffer_size)) < 0) + { + g_warning("alsa_setup(): snd_pcm_hw_params_get_buffer_size() " + "failed: %s", + snd_strerror(-err)); + return -1; + } + + if ((err = snd_pcm_hw_params_get_period_size(hwparams, &alsa_period_size, 0)) < 0) + { + g_warning("alsa_setup(): snd_pcm_hw_params_get_period_size() " + "failed: %s", + snd_strerror(-err)); + return -1; + } + + alsa_can_pause = snd_pcm_hw_params_can_pause(hwparams); + + snd_pcm_sw_params_alloca(&swparams); + snd_pcm_sw_params_current(alsa_pcm, swparams); + + /* This has effect for non-mmap only */ + if ((err = snd_pcm_sw_params_set_start_threshold(alsa_pcm, + swparams, alsa_buffer_size - alsa_period_size) < 0)) + g_warning("alsa_setup(): setting start " + "threshold failed: %s", snd_strerror(-err)); + if (snd_pcm_sw_params(alsa_pcm, swparams) < 0) + { + g_warning("alsa_setup(): Unable to install sw params"); + return -1; + } + + if (alsa_cfg.debug) + { + snd_pcm_sw_params_dump(swparams, logs); + snd_pcm_dump(alsa_pcm, logs); + } + + hw_buffer_size = snd_pcm_frames_to_bytes(alsa_pcm, alsa_buffer_size); + hw_period_size = snd_pcm_frames_to_bytes(alsa_pcm, alsa_period_size); + if (inputf->bps != outputf->bps) + { + int align = (inputf->sample_bits * inputf->channels) / 8; + hw_buffer_size_in = ((guint64)hw_buffer_size * inputf->bps + + outputf->bps/2) / outputf->bps; + hw_period_size_in = ((guint64)hw_period_size * inputf->bps + + outputf->bps/2) / outputf->bps; + hw_buffer_size_in -= hw_buffer_size_in % align; + hw_period_size_in -= hw_period_size_in % align; + } + else + { + hw_buffer_size_in = hw_buffer_size; + hw_period_size_in = hw_period_size; + } + + debug("Device setup: buffer time: %i, size: %i.", alsa_buffer_time, + hw_buffer_size); + debug("Device setup: period time: %i, size: %i.", alsa_period_time, + hw_period_size); + debug("bits per sample: %i; frame size: %i; Bps: %i", + snd_pcm_format_physical_width(outputf->format), + (int)snd_pcm_frames_to_bytes(alsa_pcm, 1), outputf->bps); + + return 0; +} + +void alsa_tell(AFormat * fmt, gint * rate, gint * nch) +{ + (*fmt) = inputf->xmms_format; + (*rate) = inputf->rate; + (*nch) = inputf->channels; +}