From 69b41650adb254e52abe044a74c495d5ea04cd08 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 26 Jan 2006 23:34:22 +0000 Subject: * Fix an annoying problem with xmms's ALSA output driver. Thanks SuSE! svn path=/nixpkgs/trunk/; revision=4607 --- pkgs/applications/audio/xmms/alsa.patch | 1475 ++++++++++++++++++++++++++++++ pkgs/applications/audio/xmms/default.nix | 3 + 2 files changed, 1478 insertions(+) create mode 100644 pkgs/applications/audio/xmms/alsa.patch diff --git a/pkgs/applications/audio/xmms/alsa.patch b/pkgs/applications/audio/xmms/alsa.patch new file mode 100644 index 000000000000..62cf7354fd25 --- /dev/null +++ b/pkgs/applications/audio/xmms/alsa.patch @@ -0,0 +1,1475 @@ +diff -rc xmms-1.2.10-orig/Output/alsa/alsa.h xmms-1.2.10/Output/alsa/alsa.h +*** xmms-1.2.10-orig/Output/alsa/alsa.h 2004-01-11 17:27:26.000000000 +0100 +--- xmms-1.2.10/Output/alsa/alsa.h 2006-01-27 00:28:49.000000000 +0100 +*************** +*** 50,57 **** + char *mixer_device; + int buffer_time; + int period_time; + gboolean debug; +- gboolean mmap; + struct + { + int left, right; +--- 50,57 ---- + char *mixer_device; + int buffer_time; + int period_time; ++ int thread_buffer_time; + gboolean debug; + struct + { + int left, right; +*************** +*** 65,72 **** + void alsa_about(void); + void alsa_configure(void); + int alsa_get_mixer(snd_mixer_t **mixer, int card); +- snd_mixer_elem_t* alsa_get_mixer_elem(snd_mixer_t *mixer, char *name, int index); +- int alsa_setup_mixer(void); + void alsa_save_config(void); + + void alsa_get_volume(int *l, int *r); +--- 65,70 ---- +diff -rc xmms-1.2.10-orig/Output/alsa/audio.c xmms-1.2.10/Output/alsa/audio.c +*** xmms-1.2.10-orig/Output/alsa/audio.c 2004-01-28 00:09:39.000000000 +0100 +--- xmms-1.2.10/Output/alsa/audio.c 2006-01-27 00:28:49.000000000 +0100 +*************** +*** 17,52 **** + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + + #include "alsa.h" + #include + #include + + static snd_pcm_t *alsa_pcm = NULL; +- static snd_pcm_status_t *alsa_status = NULL; +- static snd_pcm_channel_area_t *areas = NULL; + + static snd_output_t *logs = NULL; + +! static int alsa_bps = 0; +! static guint64 alsa_total_written = 0; + + /* Set/Get volume */ + static snd_mixer_elem_t *pcm_element = NULL; + static snd_mixer_t *mixer = NULL; + +! static gboolean mmap, force_start, going, paused; + +! static gpointer buffer; + +- static int alsa_can_pause; + + struct snd_format { + unsigned int rate; + unsigned int channels; + snd_pcm_format_t format; + AFormat xmms_format; + }; + + static struct snd_format *inputf = NULL; +--- 17,72 ---- + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ++ * ++ * ++ * CHANGES ++ * ++ * 2005.01.05 Takashi Iwai ++ * Impelemented the multi-threaded mode with an audio-thread. ++ * Many fixes and cleanups. + */ + + #include "alsa.h" + #include ++ #include + #include + + static snd_pcm_t *alsa_pcm = NULL; + + static snd_output_t *logs = NULL; + +! static guint64 alsa_total_written = 0; /* input bytes */ +! static guint64 alsa_hw_written = 0; /* output bytes */ +! static gint output_time_offset = 0; +! +! /* 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 = NULL; + static snd_mixer_t *mixer = NULL; + +! static gboolean going, paused; + +! static gboolean alsa_can_pause; +! +! /* for audio thread */ +! static pthread_t 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 gint flush_request; /* flush status (time) currently requested */ + + + 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; +*************** +*** 54,61 **** + static struct snd_format *outputf = NULL; + + static int alsa_setup(struct snd_format *f); +! static void alsa_mmap_audio(char *data, int length); +! static void alsa_write_audio(gpointer data, int length); + + static struct snd_format * snd_format_from_xmms(AFormat fmt, int rate, int channels); + +--- 74,80 ---- + static struct snd_format *outputf = NULL; + + static int alsa_setup(struct snd_format *f); +! static void alsa_write_audio(char *data, int length); + + static struct snd_format * snd_format_from_xmms(AFormat fmt, int rate, int channels); + +*************** +*** 71,92 **** + } format_table[] = + {{FMT_S16_LE, SND_PCM_FORMAT_S16_LE}, + {FMT_S16_BE, SND_PCM_FORMAT_S16_BE}, +! {FMT_S16_NE, +! #ifdef WORDS_BIGENDIAN +! SND_PCM_FORMAT_S16_BE +! #else +! SND_PCM_FORMAT_S16_LE +! #endif +! }, + {FMT_U16_LE, SND_PCM_FORMAT_U16_LE}, + {FMT_U16_BE, SND_PCM_FORMAT_U16_BE}, +! {FMT_U16_NE, +! #ifdef WORDS_BIGENDIAN +! SND_PCM_FORMAT_U16_BE +! #else +! SND_PCM_FORMAT_U16_LE +! #endif +! }, + {FMT_U8, SND_PCM_FORMAT_U8}, + {FMT_S8, SND_PCM_FORMAT_S8}, + }; +--- 90,99 ---- + } 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}, + }; +*************** +*** 106,281 **** + } + } + +! int alsa_playing(void) +! { +! if (!going || paused) +! return FALSE; +! +! return(snd_pcm_state(alsa_pcm) == SND_PCM_STATE_RUNNING); +! } +! +! static void xrun_recover(void) +! { +! int err; +! +! if (alsa_cfg.debug) +! { +! snd_pcm_status_alloca(&alsa_status); +! if ((err = 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); +! } +! } +! +! if (snd_pcm_state(alsa_pcm) == SND_PCM_STATE_XRUN) +! { +! if ((err = snd_pcm_prepare(alsa_pcm)) < 0) +! g_warning("xrun_recover(): snd_pcm_prepare() failed."); +! } +! } +! +! static snd_pcm_sframes_t alsa_get_avail(void) +! { +! snd_pcm_sframes_t ret; +! if ((ret = snd_pcm_avail_update(alsa_pcm)) == -EPIPE) +! xrun_recover(); +! else if (ret < 0) +! { +! g_warning("alsa_get_avail(): snd_pcm_avail_update() failed: %s", +! snd_strerror(-ret)); +! return 0; +! } +! else +! return ret; +! if ((ret = snd_pcm_avail_update(alsa_pcm)) < 0) +! { +! g_warning("alsa_get_avail(): snd_pcm_avail_update() failed: %s", +! snd_strerror(-ret)); +! return 0; +! } +! return ret; +! } +! +! int alsa_free(void) +! { +! if (paused) +! return 0; +! else +! { +! int err; +! if (force_start && +! snd_pcm_state(alsa_pcm) == SND_PCM_STATE_PREPARED) +! { +! if ((err = snd_pcm_start(alsa_pcm)) < 0) +! g_warning("alsa_free(): snd_pcm_start() " +! "failed: %s", snd_strerror(-err)); +! else +! debug("Stream started"); +! } +! force_start = TRUE; +! +! return snd_pcm_frames_to_bytes(alsa_pcm, alsa_get_avail()); +! } +! } +! +! void alsa_pause(short p) +! { +! debug("alsa_pause"); +! if (p) +! paused = TRUE; +! +! if (alsa_can_pause) +! snd_pcm_pause(alsa_pcm, p); +! else if (p) +! snd_pcm_drop(alsa_pcm); +! +! if (!p) +! paused = FALSE; +! } +! +! void alsa_close(void) +! { +! int err, started; +! +! debug("Closing device"); +! +! started = going; +! going = 0; +! +! pcm_element = NULL; +! +! if (mixer) +! { +! snd_mixer_close(mixer); +! mixer = NULL; +! } +! +! if (alsa_pcm != NULL) +! { +! if (started) +! if ((err = snd_pcm_drop(alsa_pcm)) < 0) +! g_warning("alsa_pcm_drop() failed: %s", +! snd_strerror(-err)); +! +! if ((err = snd_pcm_close(alsa_pcm)) < 0) +! g_warning("alsa_pcm_close() failed: %s", +! snd_strerror(-err)); +! alsa_pcm = NULL; +! } +! +! if (mmap) { +! g_free(buffer); +! buffer = NULL; +! +! g_free(areas); +! areas = NULL; +! } +! +! xmms_convert_buffers_destroy(convertb); +! convertb = NULL; +! g_free(inputf); +! inputf = NULL; +! g_free(effectf); +! effectf = NULL; +! +! alsa_save_config(); +! +! debug("Device closed"); +! } +! +! static void alsa_reopen(struct snd_format *f) +! { +! unsigned int tmp = alsa_get_written_time(); +! +! if (alsa_pcm != NULL) +! { +! snd_pcm_close(alsa_pcm); +! alsa_pcm = NULL; +! } +! +! if (mmap) { +! g_free(buffer); +! buffer = NULL; +! +! g_free(areas); +! areas = NULL; +! } +! +! if (alsa_setup(f) < 0) +! g_warning("Failed to reopen the audio device"); +! +! alsa_total_written = tmp; +! snd_pcm_prepare(alsa_pcm); +! } +! +! void alsa_flush(int time) +! { +! alsa_total_written = (guint64) time * alsa_bps / 1000; +! } +! + static void parse_mixer_name(char *str, char **name, int *index) + { + char *end; +--- 113,121 ---- + } + } + +! /* +! * mixer stuff +! */ + static void parse_mixer_name(char *str, char **name, int *index) + { + char *end; +*************** +*** 337,343 **** + } + + +! 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; +--- 177,183 ---- + } + + +! 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; +*************** +*** 353,359 **** + return elem; + } + +! int alsa_setup_mixer(void) + { + char *name; + long int a, b; +--- 193,199 ---- + return elem; + } + +! static int alsa_setup_mixer(void) + { + char *name; + long int a, b; +*************** +*** 406,411 **** +--- 246,260 ---- + return 0; + } + ++ 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) + { + static gboolean first = TRUE; +*************** +*** 461,485 **** + } + + + int alsa_get_output_time(void) + { + snd_pcm_sframes_t delay; +! ssize_t db = 0; + +! if (!going) + return 0; + +! if (!snd_pcm_delay(alsa_pcm, &delay)) +! db = snd_pcm_frames_to_bytes(alsa_pcm, delay); +! +! if (db < alsa_total_written) +! return ((alsa_total_written - db) * 1000 / alsa_bps); +! return 0; + } + + int alsa_get_written_time(void) + { +! return (alsa_total_written * 1000 / alsa_bps); + } + + #define STEREO_ADJUST(type, type2, endian) \ +--- 310,512 ---- + } + + ++ /* ++ * audio stuff ++ */ ++ ++ int alsa_playing(void) ++ { ++ if (!going || paused || alsa_pcm == NULL) ++ return FALSE; ++ ++ return(snd_pcm_state(alsa_pcm) == SND_PCM_STATE_RUNNING); ++ } ++ ++ ++ /* handle generic errors */ ++ static int alsa_handle_error(int err) ++ { ++ switch (err) { ++ case -EPIPE: /* XRUN */ ++ 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); ++ ++ case -ESTRPIPE: /* suspend */ ++ while ((err = snd_pcm_resume(alsa_pcm)) == -EAGAIN) ++ sleep(1); /* wait until suspend flag is released */ ++ if (err < 0) { ++ g_warning("suspend_recover(): snd_pcm_resume() failed."); ++ return snd_pcm_prepare(alsa_pcm); ++ } ++ break; ++ } ++ ++ 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; ++ } ++ ++ /* 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; ++ } ++ } ++ ++ /* 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); ++ } ++ ++ void alsa_close(void) ++ { ++ if (! going) ++ return; ++ ++ debug("Closing device"); ++ ++ going = 0; ++ ++ pthread_join(audio_thread, NULL); ++ ++ 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"); ++ } ++ ++ /* return the size of audio data filled in the audio thread buffer */ ++ static int get_thread_buffer_filled(void) ++ { ++ int filled = wr_index - rd_index; ++ if (filled >= 0) ++ return filled; ++ return thread_buffer_size + filled; ++ } ++ ++ /* get the free space on buffer */ ++ int alsa_free(void) ++ { ++ return thread_buffer_size - get_thread_buffer_filled() - 1; ++ } ++ + int alsa_get_output_time(void) + { + snd_pcm_sframes_t delay; +! guint64 bytes = 0; + +! if (!going || alsa_pcm == NULL) + return 0; + +! if (!snd_pcm_delay(alsa_pcm, &delay)) { +! bytes = snd_pcm_frames_to_bytes(alsa_pcm, delay); +! if (alsa_hw_written < bytes) +! bytes = 0; +! else +! bytes = alsa_hw_written - bytes; +! } +! 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) \ +*************** +*** 584,636 **** + } + + +! void alsa_write(gpointer data, int length) + { +! EffectPlugin *ep; + + if (paused) + return; + +! force_start = FALSE; +! +! if (effects_enabled() && (ep = get_current_effect_plugin())) +! { +! int new_freq = inputf->rate; +! int new_chn = inputf->channels; +! AFormat f = inputf->xmms_format; + +! if (ep->query_format) +! { +! ep->query_format(&f, &new_freq, &new_chn); +! +! if (f != effectf->xmms_format || +! new_freq != effectf->rate || +! new_chn != effectf->channels) +! { +! debug("Changing audio format for effect plugin"); +! +! g_free(effectf); +! effectf = snd_format_from_xmms(f, new_freq, +! new_chn); +! alsa_reopen(effectf); +! } +! +! } + + length = ep->mod_samples(&data, length, + inputf->xmms_format, + inputf->rate, + inputf->channels); + } +- else if (effectf) +- { +- g_free(effectf); +- effectf = NULL; +- effectf = snd_format_from_xmms(inputf->xmms_format, +- inputf->rate, +- inputf->channels); +- alsa_reopen(inputf); +- } + + if (alsa_convert_func != NULL) + length = alsa_convert_func(convertb, &data, length); +--- 611,657 ---- + } + + +! /* 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 || new_freq != effectf->rate || ++ 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); +*************** +*** 644,656 **** + if (alsa_cfg.soft_volume) + volume_adjust(data, length, outputf->xmms_format, outputf->channels); + +! if (mmap) +! alsa_mmap_audio(data, length); +! else +! alsa_write_audio(data, length); + } + +! static void alsa_write_audio(gpointer data, int length) + { + snd_pcm_sframes_t written_frames; + +--- 665,693 ---- + 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; +! +! 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; + +*************** +*** 663,735 **** + { + int written = snd_pcm_frames_to_bytes(alsa_pcm, + written_frames); +- alsa_total_written += written; + length -= written; +! data = (char*) data + written; + } +! else if (written_frames == -EPIPE) +! xrun_recover(); +! else +! { +! g_warning("alsa_write_audio(): write error: %s", +! snd_strerror(-written_frames)); +! break; + } + } + } + +! static void alsa_mmap_audio(char *data, int length) + { +! int cnt = 0, err; +! snd_pcm_uframes_t offset, frames, frame; +! const snd_pcm_channel_area_t *chan_areas = areas; +! int channel_offset = 0, channel; +! ssize_t sample_size, offset_bytes, step; +! +! alsa_get_avail(); + +! while (length > 0) +! { +! frames = snd_pcm_bytes_to_frames(alsa_pcm, length); +! if ((err = snd_pcm_mmap_begin(alsa_pcm, &chan_areas, &offset, &frames) < 0)) +! g_warning("alsa_mmap_audio(): snd_pcm_mmap_begin() " +! "failed: %s", snd_strerror(-err)); +! +! cnt = snd_pcm_frames_to_bytes(alsa_pcm, frames); +! +! sample_size = snd_pcm_samples_to_bytes(alsa_pcm, 1); +! step = chan_areas[0].step / 8; +! offset_bytes = offset * step; + +! for (frame = 0; frame < frames; frame++) +! { +! for (channel = 0; channel < outputf->channels; channel++) +! { +! char *ptr = chan_areas[channel].addr; +! memcpy(ptr + chan_areas[channel].first / 8 + +! offset_bytes, +! data + channel_offset, sample_size); +! channel_offset += sample_size; + } +! offset_bytes += step; + } +- +- err = snd_pcm_mmap_commit(alsa_pcm, offset, frames); +- if (err == -EPIPE) +- xrun_recover(); +- else if (err < 0) +- g_warning("alsa_mmap_audio(): snd_pcm_mmap_commit() " +- "failed: %s", snd_strerror(-err)); +- else if (err != frames) +- g_warning("alsa_mmap_audio(): snd_pcm_mmap_commit " +- "returned %d, expected %d", err, (int)frames); +- +- alsa_total_written += cnt; +- +- length -= cnt; + } + } + + int alsa_open(AFormat fmt, int rate, int nch) + { + debug("Opening device"); +--- 700,785 ---- + { + 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; +! +! if (npfds <= 0) +! goto _error; +! pfds = alloca(sizeof(*pfds) * npfds); +! revents = alloca(sizeof(*revents) * npfds); +! while (going && alsa_pcm) { +! if (! paused && 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; + } + } ++ ++ _error: ++ alsa_close_pcm(); ++ g_free(thread_buffer); ++ thread_buffer = NULL; ++ pthread_exit(NULL); + } + ++ /* open callback */ + int alsa_open(AFormat fmt, int rate, int nch) + { + debug("Opening device"); +*************** +*** 739,746 **** + if (alsa_cfg.debug) + snd_output_stdio_attach(&logs, stdout, 0); + +- mmap = alsa_cfg.mmap; +- + if (alsa_setup(inputf) < 0) + { + alsa_close(); +--- 789,794 ---- +*************** +*** 751,763 **** + + convertb = xmms_convert_buffers_new(); + +! alsa_total_written = 0; + going = TRUE; + paused = FALSE; +- force_start = FALSE; +- +- snd_pcm_prepare(alsa_pcm); + + return 1; + } + +--- 799,823 ---- + + convertb = xmms_convert_buffers_new(); + +! output_time_offset = 0; +! alsa_total_written = alsa_hw_written = 0; + going = TRUE; + paused = FALSE; + ++ thread_buffer_size = (guint64)alsa_cfg.thread_buffer_time * 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; ++ 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; ++ ++ pthread_create(&audio_thread, NULL, alsa_loop, NULL); ++ + return 1; + } + +*************** +*** 787,792 **** +--- 847,854 ---- + + 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; + } +*************** +*** 806,812 **** + int err; + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; +! int alsa_buffer_time, bits_per_sample; + unsigned int alsa_period_time; + snd_pcm_uframes_t alsa_buffer_size, alsa_period_size; + +--- 868,874 ---- + int err; + snd_pcm_hw_params_t *hwparams; + snd_pcm_sw_params_t *swparams; +! int alsa_buffer_time; + unsigned int alsa_period_time; + snd_pcm_uframes_t alsa_buffer_size, alsa_period_size; + +*************** +*** 816,824 **** + alsa_stereo_convert_func = NULL; + alsa_frequency_convert_func = NULL; + +! outputf = snd_format_from_xmms(effectf->xmms_format, +! effectf->rate, +! effectf->channels); + + debug("Opening device: %s", alsa_cfg.pcm_device); + /* FIXME: Can snd_pcm_open() return EAGAIN? */ +--- 878,885 ---- + 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? */ +*************** +*** 829,838 **** + g_warning("alsa_setup(): Failed to open pcm device (%s): %s", + alsa_cfg.pcm_device, snd_strerror(-err)); + alsa_pcm = NULL; + return -1; + } +- snd_pcm_nonblock(alsa_pcm, FALSE); + + if (alsa_cfg.debug) + { + snd_pcm_info_t *info; +--- 890,903 ---- + 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; +*************** +*** 856,872 **** + return -1; + } + +! if (mmap && +! (err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, +! SND_PCM_ACCESS_MMAP_INTERLEAVED)) < 0) +! { +! g_message("alsa_setup(): Cannot set mmap'ed mode: %s. " +! "falling back to direct write", snd_strerror(-err)); +! mmap = 0; +! } +! +! if (!mmap && +! (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", +--- 921,927 ---- + 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", +*************** +*** 894,910 **** + break; + } + } +! if (outputf->format != effectf->format) + { + outputf->xmms_format = + format_from_alsa(outputf->format); + debug("Converting format from %d to %d", +! effectf->xmms_format, outputf->xmms_format); + if (outputf->xmms_format < 0) + return -1; + alsa_convert_func = + xmms_convert_get_func(outputf->xmms_format, +! effectf->xmms_format); + if (alsa_convert_func == NULL) + return -1; + } +--- 949,965 ---- + 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); + if (outputf->xmms_format < 0) + return -1; + alsa_convert_func = + xmms_convert_get_func(outputf->xmms_format, +! f->xmms_format); + if (alsa_convert_func == NULL) + return -1; + } +*************** +*** 918,931 **** + } + + snd_pcm_hw_params_set_channels_near(alsa_pcm, hwparams, &outputf->channels); +! if (outputf->channels != effectf->channels) + { + debug("Converting channels from %d to %d", +! effectf->channels, outputf->channels); + alsa_stereo_convert_func = + xmms_convert_get_channel_func(outputf->xmms_format, + outputf->channels, +! effectf->channels); + if (alsa_stereo_convert_func == NULL) + return -1; + } +--- 973,986 ---- + } + + 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; + } +*************** +*** 936,945 **** + g_warning("alsa_setup(): No usable samplerate available."); + return -1; + } +! if (outputf->rate != effectf->rate) + { + debug("Converting samplerate from %d to %d", +! effectf->rate, outputf->rate); + alsa_frequency_convert_func = + xmms_convert_get_frequency_func(outputf->xmms_format, + outputf->channels); +--- 991,1000 ---- + 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); +*************** +*** 947,960 **** + return -1; + } + +! 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, +--- 1002,1009 ---- + return -1; + } + +! outputf->sample_bits = snd_pcm_format_physical_width(outputf->format); +! outputf->bps = (outputf->rate * outputf->sample_bits * outputf->channels) >> 3; + + alsa_period_time = alsa_cfg.period_time * 1000; + if ((err = snd_pcm_hw_params_set_period_time_near(alsa_pcm, hwparams, +*************** +*** 965,970 **** +--- 1014,1028 ---- + return -1; + } + ++ 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; ++ } ++ + if (snd_pcm_hw_params(alsa_pcm, hwparams) < 0) + { + if (alsa_cfg.debug) +*************** +*** 1011,1037 **** + snd_pcm_dump(alsa_pcm, logs); + } + +! bits_per_sample = snd_pcm_format_physical_width(outputf->format); +! alsa_bps = (outputf->rate * bits_per_sample * outputf->channels) >> 3; +! +! if (mmap) +! { +! int chn; +! buffer = g_malloc(alsa_period_size * bits_per_sample / 8 * outputf->channels); +! areas = g_malloc0(outputf->channels * sizeof(snd_pcm_channel_area_t)); +! +! for (chn = 0; chn < outputf->channels; chn++) +! { +! areas[chn].addr = buffer; +! areas[chn].first = chn * bits_per_sample; +! areas[chn].step = outputf->channels * bits_per_sample; +! } + } + + debug("Device setup: buffer time: %i, size: %i.", alsa_buffer_time, +! snd_pcm_frames_to_bytes(alsa_pcm, alsa_buffer_size)); + debug("bits per sample: %i; frame size: %i; Bps: %i", +! bits_per_sample, snd_pcm_frames_to_bytes(alsa_pcm, 1), alsa_bps); + + return 0; + } +--- 1069,1096 ---- + 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), +! snd_pcm_frames_to_bytes(alsa_pcm, 1), outputf->bps); + + return 0; + } +diff -rc xmms-1.2.10-orig/Output/alsa/configure.c xmms-1.2.10/Output/alsa/configure.c +*** xmms-1.2.10-orig/Output/alsa/configure.c 2004-01-28 00:09:39.000000000 +0100 +--- xmms-1.2.10/Output/alsa/configure.c 2006-01-27 00:28:49.000000000 +0100 +*************** +*** 20,27 **** + #include + + static GtkWidget *configure_win = NULL; +! static GtkWidget *buffer_time_spin, *period_time_spin; +! static GtkWidget *mmap_button, *mixer_card_spin, *softvolume_toggle_button; + + static GtkWidget *devices_combo, *mixer_devices_combo; + +--- 20,27 ---- + #include + + static GtkWidget *configure_win = NULL; +! static GtkWidget *buffer_time_spin, *period_time_spin, *thread_buffer_time_spin; +! static GtkWidget *mixer_card_spin, *softvolume_toggle_button; + + static GtkWidget *devices_combo, *mixer_devices_combo; + +*************** +*** 36,42 **** + alsa_cfg.pcm_device = GET_CHARS(GTK_COMBO(devices_combo)->entry); + alsa_cfg.buffer_time = GET_SPIN_INT(buffer_time_spin); + alsa_cfg.period_time = GET_SPIN_INT(period_time_spin); +! alsa_cfg.mmap = GET_TOGGLE(mmap_button); + alsa_cfg.soft_volume = GET_TOGGLE(softvolume_toggle_button); + alsa_cfg.mixer_card = GET_SPIN_INT(mixer_card_spin); + alsa_cfg.mixer_device = GET_CHARS(GTK_COMBO(mixer_devices_combo)->entry); +--- 36,42 ---- + alsa_cfg.pcm_device = GET_CHARS(GTK_COMBO(devices_combo)->entry); + alsa_cfg.buffer_time = GET_SPIN_INT(buffer_time_spin); + alsa_cfg.period_time = GET_SPIN_INT(period_time_spin); +! alsa_cfg.thread_buffer_time = GET_SPIN_INT(thread_buffer_time_spin); + alsa_cfg.soft_volume = GET_TOGGLE(softvolume_toggle_button); + alsa_cfg.mixer_card = GET_SPIN_INT(mixer_card_spin); + alsa_cfg.mixer_device = GET_CHARS(GTK_COMBO(mixer_devices_combo)->entry); +*************** +*** 51,57 **** + + xmms_cfg_write_int(cfgfile, "ALSA", "buffer_time", alsa_cfg.buffer_time); + xmms_cfg_write_int(cfgfile, "ALSA", "period_time", alsa_cfg.period_time); +! xmms_cfg_write_boolean(cfgfile,"ALSA","mmap",alsa_cfg.mmap); + xmms_cfg_write_string(cfgfile,"ALSA","pcm_device", alsa_cfg.pcm_device); + xmms_cfg_write_int(cfgfile, "ALSA", "mixer_card", alsa_cfg.mixer_card); + xmms_cfg_write_string(cfgfile,"ALSA","mixer_device", alsa_cfg.mixer_device); +--- 51,57 ---- + + xmms_cfg_write_int(cfgfile, "ALSA", "buffer_time", alsa_cfg.buffer_time); + xmms_cfg_write_int(cfgfile, "ALSA", "period_time", alsa_cfg.period_time); +! xmms_cfg_write_int(cfgfile, "ALSA", "thread_buffer_time", alsa_cfg.thread_buffer_time); + xmms_cfg_write_string(cfgfile,"ALSA","pcm_device", alsa_cfg.pcm_device); + xmms_cfg_write_int(cfgfile, "ALSA", "mixer_card", alsa_cfg.mixer_card); + xmms_cfg_write_string(cfgfile,"ALSA","mixer_device", alsa_cfg.mixer_device); +*************** +*** 212,219 **** + GtkWidget *dev_vbox, *adevice_frame, *adevice_box; + GtkWidget *mixer_frame, *mixer_box, *mixer_card_box; + GtkWidget *buffer_frame, *buffer_vbox, *buffer_table; +! GtkWidget *buffer_time_label, *period_time_label; +! GtkObject *buffer_time_adj, *period_time_adj, *mixer_card_adj; + GtkWidget *bbox, *ok, *cancel; + + if (configure_win) +--- 212,219 ---- + GtkWidget *dev_vbox, *adevice_frame, *adevice_box; + GtkWidget *mixer_frame, *mixer_box, *mixer_card_box; + GtkWidget *buffer_frame, *buffer_vbox, *buffer_table; +! GtkWidget *buffer_time_label, *period_time_label, *thread_buffer_time_label; +! GtkObject *buffer_time_adj, *period_time_adj, *thread_buffer_time_adj, *mixer_card_adj; + GtkWidget *bbox, *ok, *cancel; + + if (configure_win) +*************** +*** 312,318 **** + + gtk_container_set_border_width(GTK_CONTAINER(buffer_vbox), 5); + +! buffer_table = gtk_table_new(2, 2, FALSE); + gtk_table_set_row_spacings(GTK_TABLE(buffer_table), 5); + gtk_table_set_col_spacings(GTK_TABLE(buffer_table), 5); + gtk_box_pack_start(GTK_BOX(buffer_vbox), buffer_table, FALSE, FALSE, 0); +--- 312,318 ---- + + gtk_container_set_border_width(GTK_CONTAINER(buffer_vbox), 5); + +! buffer_table = gtk_table_new(2, 3, FALSE); + gtk_table_set_row_spacings(GTK_TABLE(buffer_table), 5); + gtk_table_set_col_spacings(GTK_TABLE(buffer_table), 5); + gtk_box_pack_start(GTK_BOX(buffer_vbox), buffer_table, FALSE, FALSE, 0); +*************** +*** 345,354 **** + gtk_table_attach(GTK_TABLE(buffer_table), period_time_spin, + 1, 2, 1, 2, 0, 0, 0, 0); + +! mmap_button = gtk_check_button_new_with_label(_("Mmap mode")); +! gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(mmap_button), +! alsa_cfg.mmap); +! gtk_box_pack_start(GTK_BOX(buffer_vbox), mmap_button, FALSE, FALSE, 0); + + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), buffer_frame, + gtk_label_new(_("Advanced settings"))); +--- 345,363 ---- + gtk_table_attach(GTK_TABLE(buffer_table), period_time_spin, + 1, 2, 1, 2, 0, 0, 0, 0); + +! thread_buffer_time_label = gtk_label_new(_("Thread buffer time (ms):")); +! gtk_label_set_justify(GTK_LABEL(thread_buffer_time_label), GTK_JUSTIFY_LEFT); +! gtk_misc_set_alignment(GTK_MISC(thread_buffer_time_label), 0, 0.5); +! gtk_table_attach(GTK_TABLE(buffer_table), thread_buffer_time_label, +! 0, 1, 2, 3, GTK_FILL, 0, 0, 0); +! thread_buffer_time_adj = gtk_adjustment_new(alsa_cfg.thread_buffer_time, +! 1000, 1000000, 100, 100, 100); +! thread_buffer_time_spin = gtk_spin_button_new(GTK_ADJUSTMENT(thread_buffer_time_adj), +! 8, 0); +! +! gtk_widget_set_usize(thread_buffer_time_spin, 60, -1); +! gtk_table_attach(GTK_TABLE(buffer_table), thread_buffer_time_spin, +! 1, 2, 2, 3, 0, 0, 0, 0); + + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), buffer_frame, + gtk_label_new(_("Advanced settings"))); +diff -rc xmms-1.2.10-orig/Output/alsa/init.c xmms-1.2.10/Output/alsa/init.c +*** xmms-1.2.10-orig/Output/alsa/init.c 2004-01-11 17:27:26.000000000 +0100 +--- xmms-1.2.10/Output/alsa/init.c 2006-01-27 00:28:49.000000000 +0100 +*************** +*** 29,36 **** + memset(&alsa_cfg, 0, sizeof (alsa_cfg)); + alsa_cfg.buffer_time = 500; + alsa_cfg.period_time = 50; + alsa_cfg.debug = 0; +- alsa_cfg.mmap = 1; + alsa_cfg.vol.left = 100; + alsa_cfg.vol.right = 100; + +--- 29,36 ---- + memset(&alsa_cfg, 0, sizeof (alsa_cfg)); + alsa_cfg.buffer_time = 500; + alsa_cfg.period_time = 50; ++ alsa_cfg.thread_buffer_time = 3000; + alsa_cfg.debug = 0; + alsa_cfg.vol.left = 100; + alsa_cfg.vol.right = 100; + +*************** +*** 44,51 **** + xmms_cfg_read_int(cfgfile, "ALSA", "mixer_card", &alsa_cfg.mixer_card); + xmms_cfg_read_int(cfgfile, "ALSA", "buffer_time", &alsa_cfg.buffer_time); + xmms_cfg_read_int(cfgfile, "ALSA", "period_time", &alsa_cfg.period_time); +! xmms_cfg_read_boolean(cfgfile, "ALSA", "mmap", &alsa_cfg.mmap); +! xmms_cfg_read_int(cfgfile, "ALSA", "period_time", &alsa_cfg.period_time); + xmms_cfg_read_boolean(cfgfile, "ALSA", "soft_volume", + &alsa_cfg.soft_volume); + xmms_cfg_read_int(cfgfile, "ALSA", "volume_left", &alsa_cfg.vol.left); +--- 44,50 ---- + xmms_cfg_read_int(cfgfile, "ALSA", "mixer_card", &alsa_cfg.mixer_card); + xmms_cfg_read_int(cfgfile, "ALSA", "buffer_time", &alsa_cfg.buffer_time); + xmms_cfg_read_int(cfgfile, "ALSA", "period_time", &alsa_cfg.period_time); +! xmms_cfg_read_int(cfgfile, "ALSA", "thread_buffer_time", &alsa_cfg.thread_buffer_time); + xmms_cfg_read_boolean(cfgfile, "ALSA", "soft_volume", + &alsa_cfg.soft_volume); + xmms_cfg_read_int(cfgfile, "ALSA", "volume_left", &alsa_cfg.vol.left); diff --git a/pkgs/applications/audio/xmms/default.nix b/pkgs/applications/audio/xmms/default.nix index a0e2d4729179..a7743b861ebb 100644 --- a/pkgs/applications/audio/xmms/default.nix +++ b/pkgs/applications/audio/xmms/default.nix @@ -7,5 +7,8 @@ stdenv.mkDerivation { md5 = "03a85cfc5e1877a2e1f7be4fa1d3f63c" ; }; + # Patch borrowed from SuSE 10.0 to fix pause/continue on ALSA. + patches = [./alsa.patch]; + buildInputs = [alsaLib esound libogg libvorbis glib gtk]; } -- cgit 1.4.1