diff options
Diffstat (limited to 'audio')
-rw-r--r-- | audio/alsaaudio.c | 946 | ||||
-rw-r--r-- | audio/audio.c | 2222 | ||||
-rw-r--r-- | audio/audio.h | 181 | ||||
-rw-r--r-- | audio/audio_int.h | 287 | ||||
-rw-r--r-- | audio/audio_legacy.c | 555 | ||||
-rw-r--r-- | audio/audio_template.h | 568 | ||||
-rw-r--r-- | audio/audio_win_int.c | 131 | ||||
-rw-r--r-- | audio/audio_win_int.h | 10 | ||||
-rw-r--r-- | audio/coreaudio.c | 681 | ||||
-rw-r--r-- | audio/dsound_template.h | 280 | ||||
-rw-r--r-- | audio/dsoundaudio.c | 732 | ||||
-rw-r--r-- | audio/jackaudio.c | 694 | ||||
-rw-r--r-- | audio/meson.build | 29 | ||||
-rw-r--r-- | audio/mixeng.c | 470 | ||||
-rw-r--r-- | audio/mixeng.h | 58 | ||||
-rw-r--r-- | audio/mixeng_template.h | 152 | ||||
-rw-r--r-- | audio/noaudio.c | 148 | ||||
-rw-r--r-- | audio/ossaudio.c | 782 | ||||
-rw-r--r-- | audio/paaudio.c | 933 | ||||
-rw-r--r-- | audio/rate_template.h | 117 | ||||
-rw-r--r-- | audio/sdlaudio.c | 508 | ||||
-rw-r--r-- | audio/spiceaudio.c | 321 | ||||
-rw-r--r-- | audio/trace-events | 19 | ||||
-rw-r--r-- | audio/trace.h | 1 | ||||
-rw-r--r-- | audio/wavaudio.c | 221 | ||||
-rw-r--r-- | audio/wavcapture.c | 191 |
26 files changed, 11237 insertions, 0 deletions
diff --git a/audio/alsaaudio.c b/audio/alsaaudio.c new file mode 100644 index 000000000..2b9789e64 --- /dev/null +++ b/audio/alsaaudio.c @@ -0,0 +1,946 @@ +/* + * QEMU ALSA audio driver + * + * Copyright (c) 2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include <alsa/asoundlib.h> +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "audio.h" +#include "trace.h" + +#pragma GCC diagnostic ignored "-Waddress" + +#define AUDIO_CAP "alsa" +#include "audio_int.h" + +#define DEBUG_ALSA 0 + +struct pollhlp { + snd_pcm_t *handle; + struct pollfd *pfds; + int count; + int mask; + AudioState *s; +}; + +typedef struct ALSAVoiceOut { + HWVoiceOut hw; + snd_pcm_t *handle; + struct pollhlp pollhlp; + Audiodev *dev; +} ALSAVoiceOut; + +typedef struct ALSAVoiceIn { + HWVoiceIn hw; + snd_pcm_t *handle; + struct pollhlp pollhlp; + Audiodev *dev; +} ALSAVoiceIn; + +struct alsa_params_req { + int freq; + snd_pcm_format_t fmt; + int nchannels; +}; + +struct alsa_params_obt { + int freq; + AudioFormat fmt; + int endianness; + int nchannels; + snd_pcm_uframes_t samples; +}; + +static void GCC_FMT_ATTR (2, 3) alsa_logerr (int err, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", snd_strerror (err)); +} + +static void GCC_FMT_ATTR (3, 4) alsa_logerr2 ( + int err, + const char *typ, + const char *fmt, + ... + ) +{ + va_list ap; + + AUD_log (AUDIO_CAP, "Could not initialize %s\n", typ); + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", snd_strerror (err)); +} + +static void alsa_fini_poll (struct pollhlp *hlp) +{ + int i; + struct pollfd *pfds = hlp->pfds; + + if (pfds) { + for (i = 0; i < hlp->count; ++i) { + qemu_set_fd_handler (pfds[i].fd, NULL, NULL, NULL); + } + g_free (pfds); + } + hlp->pfds = NULL; + hlp->count = 0; + hlp->handle = NULL; +} + +static void alsa_anal_close1 (snd_pcm_t **handlep) +{ + int err = snd_pcm_close (*handlep); + if (err) { + alsa_logerr (err, "Failed to close PCM handle %p\n", *handlep); + } + *handlep = NULL; +} + +static void alsa_anal_close (snd_pcm_t **handlep, struct pollhlp *hlp) +{ + alsa_fini_poll (hlp); + alsa_anal_close1 (handlep); +} + +static int alsa_recover (snd_pcm_t *handle) +{ + int err = snd_pcm_prepare (handle); + if (err < 0) { + alsa_logerr (err, "Failed to prepare handle %p\n", handle); + return -1; + } + return 0; +} + +static int alsa_resume (snd_pcm_t *handle) +{ + int err = snd_pcm_resume (handle); + if (err < 0) { + alsa_logerr (err, "Failed to resume handle %p\n", handle); + return -1; + } + return 0; +} + +static void alsa_poll_handler (void *opaque) +{ + int err, count; + snd_pcm_state_t state; + struct pollhlp *hlp = opaque; + unsigned short revents; + + count = poll (hlp->pfds, hlp->count, 0); + if (count < 0) { + dolog ("alsa_poll_handler: poll %s\n", strerror (errno)); + return; + } + + if (!count) { + return; + } + + /* XXX: ALSA example uses initial count, not the one returned by + poll, correct? */ + err = snd_pcm_poll_descriptors_revents (hlp->handle, hlp->pfds, + hlp->count, &revents); + if (err < 0) { + alsa_logerr (err, "snd_pcm_poll_descriptors_revents"); + return; + } + + if (!(revents & hlp->mask)) { + trace_alsa_revents(revents); + return; + } + + state = snd_pcm_state (hlp->handle); + switch (state) { + case SND_PCM_STATE_SETUP: + alsa_recover (hlp->handle); + break; + + case SND_PCM_STATE_XRUN: + alsa_recover (hlp->handle); + break; + + case SND_PCM_STATE_SUSPENDED: + alsa_resume (hlp->handle); + break; + + case SND_PCM_STATE_PREPARED: + audio_run(hlp->s, "alsa run (prepared)"); + break; + + case SND_PCM_STATE_RUNNING: + audio_run(hlp->s, "alsa run (running)"); + break; + + default: + dolog ("Unexpected state %d\n", state); + } +} + +static int alsa_poll_helper (snd_pcm_t *handle, struct pollhlp *hlp, int mask) +{ + int i, count, err; + struct pollfd *pfds; + + count = snd_pcm_poll_descriptors_count (handle); + if (count <= 0) { + dolog ("Could not initialize poll mode\n" + "Invalid number of poll descriptors %d\n", count); + return -1; + } + + pfds = audio_calloc ("alsa_poll_helper", count, sizeof (*pfds)); + if (!pfds) { + dolog ("Could not initialize poll mode\n"); + return -1; + } + + err = snd_pcm_poll_descriptors (handle, pfds, count); + if (err < 0) { + alsa_logerr (err, "Could not initialize poll mode\n" + "Could not obtain poll descriptors\n"); + g_free (pfds); + return -1; + } + + for (i = 0; i < count; ++i) { + if (pfds[i].events & POLLIN) { + qemu_set_fd_handler (pfds[i].fd, alsa_poll_handler, NULL, hlp); + } + if (pfds[i].events & POLLOUT) { + trace_alsa_pollout(i, pfds[i].fd); + qemu_set_fd_handler (pfds[i].fd, NULL, alsa_poll_handler, hlp); + } + trace_alsa_set_handler(pfds[i].events, i, pfds[i].fd, err); + + } + hlp->pfds = pfds; + hlp->count = count; + hlp->handle = handle; + hlp->mask = mask; + return 0; +} + +static int alsa_poll_out (HWVoiceOut *hw) +{ + ALSAVoiceOut *alsa = (ALSAVoiceOut *) hw; + + return alsa_poll_helper (alsa->handle, &alsa->pollhlp, POLLOUT); +} + +static int alsa_poll_in (HWVoiceIn *hw) +{ + ALSAVoiceIn *alsa = (ALSAVoiceIn *) hw; + + return alsa_poll_helper (alsa->handle, &alsa->pollhlp, POLLIN); +} + +static snd_pcm_format_t aud_to_alsafmt (AudioFormat fmt, int endianness) +{ + switch (fmt) { + case AUDIO_FORMAT_S8: + return SND_PCM_FORMAT_S8; + + case AUDIO_FORMAT_U8: + return SND_PCM_FORMAT_U8; + + case AUDIO_FORMAT_S16: + if (endianness) { + return SND_PCM_FORMAT_S16_BE; + } else { + return SND_PCM_FORMAT_S16_LE; + } + + case AUDIO_FORMAT_U16: + if (endianness) { + return SND_PCM_FORMAT_U16_BE; + } else { + return SND_PCM_FORMAT_U16_LE; + } + + case AUDIO_FORMAT_S32: + if (endianness) { + return SND_PCM_FORMAT_S32_BE; + } else { + return SND_PCM_FORMAT_S32_LE; + } + + case AUDIO_FORMAT_U32: + if (endianness) { + return SND_PCM_FORMAT_U32_BE; + } else { + return SND_PCM_FORMAT_U32_LE; + } + + case AUDIO_FORMAT_F32: + if (endianness) { + return SND_PCM_FORMAT_FLOAT_BE; + } else { + return SND_PCM_FORMAT_FLOAT_LE; + } + + default: + dolog ("Internal logic error: Bad audio format %d\n", fmt); +#ifdef DEBUG_AUDIO + abort (); +#endif + return SND_PCM_FORMAT_U8; + } +} + +static int alsa_to_audfmt (snd_pcm_format_t alsafmt, AudioFormat *fmt, + int *endianness) +{ + switch (alsafmt) { + case SND_PCM_FORMAT_S8: + *endianness = 0; + *fmt = AUDIO_FORMAT_S8; + break; + + case SND_PCM_FORMAT_U8: + *endianness = 0; + *fmt = AUDIO_FORMAT_U8; + break; + + case SND_PCM_FORMAT_S16_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_S16; + break; + + case SND_PCM_FORMAT_U16_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_U16; + break; + + case SND_PCM_FORMAT_S16_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_S16; + break; + + case SND_PCM_FORMAT_U16_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_U16; + break; + + case SND_PCM_FORMAT_S32_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_S32; + break; + + case SND_PCM_FORMAT_U32_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_U32; + break; + + case SND_PCM_FORMAT_S32_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_S32; + break; + + case SND_PCM_FORMAT_U32_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_U32; + break; + + case SND_PCM_FORMAT_FLOAT_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_F32; + break; + + case SND_PCM_FORMAT_FLOAT_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_F32; + break; + + default: + dolog ("Unrecognized audio format %d\n", alsafmt); + return -1; + } + + return 0; +} + +static void alsa_dump_info (struct alsa_params_req *req, + struct alsa_params_obt *obt, + snd_pcm_format_t obtfmt, + AudiodevAlsaPerDirectionOptions *apdo) +{ + dolog("parameter | requested value | obtained value\n"); + dolog("format | %10d | %10d\n", req->fmt, obtfmt); + dolog("channels | %10d | %10d\n", + req->nchannels, obt->nchannels); + dolog("frequency | %10d | %10d\n", req->freq, obt->freq); + dolog("============================================\n"); + dolog("requested: buffer len %" PRId32 " period len %" PRId32 "\n", + apdo->buffer_length, apdo->period_length); + dolog("obtained: samples %ld\n", obt->samples); +} + +static void alsa_set_threshold (snd_pcm_t *handle, snd_pcm_uframes_t threshold) +{ + int err; + snd_pcm_sw_params_t *sw_params; + + snd_pcm_sw_params_alloca (&sw_params); + + err = snd_pcm_sw_params_current (handle, sw_params); + if (err < 0) { + dolog ("Could not fully initialize DAC\n"); + alsa_logerr (err, "Failed to get current software parameters\n"); + return; + } + + err = snd_pcm_sw_params_set_start_threshold (handle, sw_params, threshold); + if (err < 0) { + dolog ("Could not fully initialize DAC\n"); + alsa_logerr (err, "Failed to set software threshold to %ld\n", + threshold); + return; + } + + err = snd_pcm_sw_params (handle, sw_params); + if (err < 0) { + dolog ("Could not fully initialize DAC\n"); + alsa_logerr (err, "Failed to set software parameters\n"); + return; + } +} + +static int alsa_open(bool in, struct alsa_params_req *req, + struct alsa_params_obt *obt, snd_pcm_t **handlep, + Audiodev *dev) +{ + AudiodevAlsaOptions *aopts = &dev->u.alsa; + AudiodevAlsaPerDirectionOptions *apdo = in ? aopts->in : aopts->out; + snd_pcm_t *handle; + snd_pcm_hw_params_t *hw_params; + int err; + unsigned int freq, nchannels; + const char *pcm_name = apdo->has_dev ? apdo->dev : "default"; + snd_pcm_uframes_t obt_buffer_size; + const char *typ = in ? "ADC" : "DAC"; + snd_pcm_format_t obtfmt; + + freq = req->freq; + nchannels = req->nchannels; + + snd_pcm_hw_params_alloca (&hw_params); + + err = snd_pcm_open ( + &handle, + pcm_name, + in ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, + SND_PCM_NONBLOCK + ); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to open `%s':\n", pcm_name); + return -1; + } + + err = snd_pcm_hw_params_any (handle, hw_params); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to initialize hardware parameters\n"); + goto err; + } + + err = snd_pcm_hw_params_set_access ( + handle, + hw_params, + SND_PCM_ACCESS_RW_INTERLEAVED + ); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to set access type\n"); + goto err; + } + + err = snd_pcm_hw_params_set_format (handle, hw_params, req->fmt); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to set format %d\n", req->fmt); + } + + err = snd_pcm_hw_params_set_rate_near (handle, hw_params, &freq, 0); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to set frequency %d\n", req->freq); + goto err; + } + + err = snd_pcm_hw_params_set_channels_near ( + handle, + hw_params, + &nchannels + ); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to set number of channels %d\n", + req->nchannels); + goto err; + } + + if (apdo->buffer_length) { + int dir = 0; + unsigned int btime = apdo->buffer_length; + + err = snd_pcm_hw_params_set_buffer_time_near( + handle, hw_params, &btime, &dir); + + if (err < 0) { + alsa_logerr2(err, typ, "Failed to set buffer time to %" PRId32 "\n", + apdo->buffer_length); + goto err; + } + + if (apdo->has_buffer_length && btime != apdo->buffer_length) { + dolog("Requested buffer time %" PRId32 + " was rejected, using %u\n", apdo->buffer_length, btime); + } + } + + if (apdo->period_length) { + int dir = 0; + unsigned int ptime = apdo->period_length; + + err = snd_pcm_hw_params_set_period_time_near(handle, hw_params, &ptime, + &dir); + + if (err < 0) { + alsa_logerr2(err, typ, "Failed to set period time to %" PRId32 "\n", + apdo->period_length); + goto err; + } + + if (apdo->has_period_length && ptime != apdo->period_length) { + dolog("Requested period time %" PRId32 " was rejected, using %d\n", + apdo->period_length, ptime); + } + } + + err = snd_pcm_hw_params (handle, hw_params); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to apply audio parameters\n"); + goto err; + } + + err = snd_pcm_hw_params_get_buffer_size (hw_params, &obt_buffer_size); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to get buffer size\n"); + goto err; + } + + err = snd_pcm_hw_params_get_format (hw_params, &obtfmt); + if (err < 0) { + alsa_logerr2 (err, typ, "Failed to get format\n"); + goto err; + } + + if (alsa_to_audfmt (obtfmt, &obt->fmt, &obt->endianness)) { + dolog ("Invalid format was returned %d\n", obtfmt); + goto err; + } + + err = snd_pcm_prepare (handle); + if (err < 0) { + alsa_logerr2 (err, typ, "Could not prepare handle %p\n", handle); + goto err; + } + + if (!in && aopts->has_threshold && aopts->threshold) { + struct audsettings as = { .freq = freq }; + alsa_set_threshold( + handle, + audio_buffer_frames(qapi_AudiodevAlsaPerDirectionOptions_base(apdo), + &as, aopts->threshold)); + } + + obt->nchannels = nchannels; + obt->freq = freq; + obt->samples = obt_buffer_size; + + *handlep = handle; + + if (DEBUG_ALSA || obtfmt != req->fmt || + obt->nchannels != req->nchannels || obt->freq != req->freq) { + dolog ("Audio parameters for %s\n", typ); + alsa_dump_info(req, obt, obtfmt, apdo); + } + + return 0; + + err: + alsa_anal_close1 (&handle); + return -1; +} + +static size_t alsa_write(HWVoiceOut *hw, void *buf, size_t len) +{ + ALSAVoiceOut *alsa = (ALSAVoiceOut *) hw; + size_t pos = 0; + size_t len_frames = len / hw->info.bytes_per_frame; + + while (len_frames) { + char *src = advance(buf, pos); + snd_pcm_sframes_t written; + + written = snd_pcm_writei(alsa->handle, src, len_frames); + + if (written <= 0) { + switch (written) { + case 0: + trace_alsa_wrote_zero(len_frames); + return pos; + + case -EPIPE: + if (alsa_recover(alsa->handle)) { + alsa_logerr(written, "Failed to write %zu frames\n", + len_frames); + return pos; + } + trace_alsa_xrun_out(); + continue; + + case -ESTRPIPE: + /* + * stream is suspended and waiting for an application + * recovery + */ + if (alsa_resume(alsa->handle)) { + alsa_logerr(written, "Failed to write %zu frames\n", + len_frames); + return pos; + } + trace_alsa_resume_out(); + continue; + + case -EAGAIN: + return pos; + + default: + alsa_logerr(written, "Failed to write %zu frames from %p\n", + len, src); + return pos; + } + } + + pos += written * hw->info.bytes_per_frame; + if (written < len_frames) { + break; + } + len_frames -= written; + } + + return pos; +} + +static void alsa_fini_out (HWVoiceOut *hw) +{ + ALSAVoiceOut *alsa = (ALSAVoiceOut *) hw; + + ldebug ("alsa_fini\n"); + alsa_anal_close (&alsa->handle, &alsa->pollhlp); +} + +static int alsa_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + ALSAVoiceOut *alsa = (ALSAVoiceOut *) hw; + struct alsa_params_req req; + struct alsa_params_obt obt; + snd_pcm_t *handle; + struct audsettings obt_as; + Audiodev *dev = drv_opaque; + + req.fmt = aud_to_alsafmt (as->fmt, as->endianness); + req.freq = as->freq; + req.nchannels = as->nchannels; + + if (alsa_open(0, &req, &obt, &handle, dev)) { + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.nchannels; + obt_as.fmt = obt.fmt; + obt_as.endianness = obt.endianness; + + audio_pcm_init_info (&hw->info, &obt_as); + hw->samples = obt.samples; + + alsa->pollhlp.s = hw->s; + alsa->handle = handle; + alsa->dev = dev; + return 0; +} + +#define VOICE_CTL_PAUSE 0 +#define VOICE_CTL_PREPARE 1 +#define VOICE_CTL_START 2 + +static int alsa_voice_ctl (snd_pcm_t *handle, const char *typ, int ctl) +{ + int err; + + if (ctl == VOICE_CTL_PAUSE) { + err = snd_pcm_drop (handle); + if (err < 0) { + alsa_logerr (err, "Could not stop %s\n", typ); + return -1; + } + } else { + err = snd_pcm_prepare (handle); + if (err < 0) { + alsa_logerr (err, "Could not prepare handle for %s\n", typ); + return -1; + } + if (ctl == VOICE_CTL_START) { + err = snd_pcm_start(handle); + if (err < 0) { + alsa_logerr (err, "Could not start handle for %s\n", typ); + return -1; + } + } + } + + return 0; +} + +static void alsa_enable_out(HWVoiceOut *hw, bool enable) +{ + ALSAVoiceOut *alsa = (ALSAVoiceOut *) hw; + AudiodevAlsaPerDirectionOptions *apdo = alsa->dev->u.alsa.out; + + if (enable) { + bool poll_mode = apdo->try_poll; + + ldebug("enabling voice\n"); + if (poll_mode && alsa_poll_out(hw)) { + poll_mode = 0; + } + hw->poll_mode = poll_mode; + alsa_voice_ctl(alsa->handle, "playback", VOICE_CTL_PREPARE); + } else { + ldebug("disabling voice\n"); + if (hw->poll_mode) { + hw->poll_mode = 0; + alsa_fini_poll(&alsa->pollhlp); + } + alsa_voice_ctl(alsa->handle, "playback", VOICE_CTL_PAUSE); + } +} + +static int alsa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + ALSAVoiceIn *alsa = (ALSAVoiceIn *) hw; + struct alsa_params_req req; + struct alsa_params_obt obt; + snd_pcm_t *handle; + struct audsettings obt_as; + Audiodev *dev = drv_opaque; + + req.fmt = aud_to_alsafmt (as->fmt, as->endianness); + req.freq = as->freq; + req.nchannels = as->nchannels; + + if (alsa_open(1, &req, &obt, &handle, dev)) { + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.nchannels; + obt_as.fmt = obt.fmt; + obt_as.endianness = obt.endianness; + + audio_pcm_init_info (&hw->info, &obt_as); + hw->samples = obt.samples; + + alsa->pollhlp.s = hw->s; + alsa->handle = handle; + alsa->dev = dev; + return 0; +} + +static void alsa_fini_in (HWVoiceIn *hw) +{ + ALSAVoiceIn *alsa = (ALSAVoiceIn *) hw; + + alsa_anal_close (&alsa->handle, &alsa->pollhlp); +} + +static size_t alsa_read(HWVoiceIn *hw, void *buf, size_t len) +{ + ALSAVoiceIn *alsa = (ALSAVoiceIn *) hw; + size_t pos = 0; + + while (len) { + void *dst = advance(buf, pos); + snd_pcm_sframes_t nread; + + nread = snd_pcm_readi( + alsa->handle, dst, len / hw->info.bytes_per_frame); + + if (nread <= 0) { + switch (nread) { + case 0: + trace_alsa_read_zero(len); + return pos; + + case -EPIPE: + if (alsa_recover(alsa->handle)) { + alsa_logerr(nread, "Failed to read %zu frames\n", len); + return pos; + } + trace_alsa_xrun_in(); + continue; + + case -EAGAIN: + return pos; + + default: + alsa_logerr(nread, "Failed to read %zu frames to %p\n", + len, dst); + return pos; + } + } + + pos += nread * hw->info.bytes_per_frame; + len -= nread * hw->info.bytes_per_frame; + } + + return pos; +} + +static void alsa_enable_in(HWVoiceIn *hw, bool enable) +{ + ALSAVoiceIn *alsa = (ALSAVoiceIn *) hw; + AudiodevAlsaPerDirectionOptions *apdo = alsa->dev->u.alsa.in; + + if (enable) { + bool poll_mode = apdo->try_poll; + + ldebug("enabling voice\n"); + if (poll_mode && alsa_poll_in(hw)) { + poll_mode = 0; + } + hw->poll_mode = poll_mode; + + alsa_voice_ctl(alsa->handle, "capture", VOICE_CTL_START); + } else { + ldebug ("disabling voice\n"); + if (hw->poll_mode) { + hw->poll_mode = 0; + alsa_fini_poll(&alsa->pollhlp); + } + alsa_voice_ctl(alsa->handle, "capture", VOICE_CTL_PAUSE); + } +} + +static void alsa_init_per_direction(AudiodevAlsaPerDirectionOptions *apdo) +{ + if (!apdo->has_try_poll) { + apdo->try_poll = true; + apdo->has_try_poll = true; + } +} + +static void *alsa_audio_init(Audiodev *dev) +{ + AudiodevAlsaOptions *aopts; + assert(dev->driver == AUDIODEV_DRIVER_ALSA); + + aopts = &dev->u.alsa; + alsa_init_per_direction(aopts->in); + alsa_init_per_direction(aopts->out); + + /* + * need to define them, as otherwise alsa produces no sound + * doesn't set has_* so alsa_open can identify it wasn't set by the user + */ + if (!dev->u.alsa.out->has_period_length) { + /* 1024 frames assuming 44100Hz */ + dev->u.alsa.out->period_length = 1024 * 1000000 / 44100; + } + if (!dev->u.alsa.out->has_buffer_length) { + /* 4096 frames assuming 44100Hz */ + dev->u.alsa.out->buffer_length = 4096ll * 1000000 / 44100; + } + + /* + * OptsVisitor sets unspecified optional fields to zero, but do not depend + * on it... + */ + if (!dev->u.alsa.in->has_period_length) { + dev->u.alsa.in->period_length = 0; + } + if (!dev->u.alsa.in->has_buffer_length) { + dev->u.alsa.in->buffer_length = 0; + } + + return dev; +} + +static void alsa_audio_fini (void *opaque) +{ +} + +static struct audio_pcm_ops alsa_pcm_ops = { + .init_out = alsa_init_out, + .fini_out = alsa_fini_out, + .write = alsa_write, + .run_buffer_out = audio_generic_run_buffer_out, + .enable_out = alsa_enable_out, + + .init_in = alsa_init_in, + .fini_in = alsa_fini_in, + .read = alsa_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = alsa_enable_in, +}; + +static struct audio_driver alsa_audio_driver = { + .name = "alsa", + .descr = "ALSA http://www.alsa-project.org", + .init = alsa_audio_init, + .fini = alsa_audio_fini, + .pcm_ops = &alsa_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof (ALSAVoiceOut), + .voice_size_in = sizeof (ALSAVoiceIn) +}; + +static void register_audio_alsa(void) +{ + audio_driver_register(&alsa_audio_driver); +} +type_init(register_audio_alsa); diff --git a/audio/audio.c b/audio/audio.c new file mode 100644 index 000000000..54a153c0e --- /dev/null +++ b/audio/audio.c @@ -0,0 +1,2222 @@ +/* + * QEMU Audio subsystem + * + * Copyright (c) 2003-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "audio.h" +#include "migration/vmstate.h" +#include "monitor/monitor.h" +#include "qemu/timer.h" +#include "qapi/error.h" +#include "qapi/qobject-input-visitor.h" +#include "qapi/qapi-visit-audio.h" +#include "qemu/cutils.h" +#include "qemu/module.h" +#include "qemu-common.h" +#include "sysemu/replay.h" +#include "sysemu/runstate.h" +#include "ui/qemu-spice.h" +#include "trace.h" + +#define AUDIO_CAP "audio" +#include "audio_int.h" + +/* #define DEBUG_LIVE */ +/* #define DEBUG_OUT */ +/* #define DEBUG_CAPTURE */ +/* #define DEBUG_POLL */ + +#define SW_NAME(sw) (sw)->name ? (sw)->name : "unknown" + + +/* Order of CONFIG_AUDIO_DRIVERS is import. + The 1st one is the one used by default, that is the reason + that we generate the list. +*/ +const char *audio_prio_list[] = { + "spice", + CONFIG_AUDIO_DRIVERS + "none", + "wav", + NULL +}; + +static QLIST_HEAD(, audio_driver) audio_drivers; +static AudiodevListHead audiodevs = QSIMPLEQ_HEAD_INITIALIZER(audiodevs); + +void audio_driver_register(audio_driver *drv) +{ + QLIST_INSERT_HEAD(&audio_drivers, drv, next); +} + +audio_driver *audio_driver_lookup(const char *name) +{ + struct audio_driver *d; + + QLIST_FOREACH(d, &audio_drivers, next) { + if (strcmp(name, d->name) == 0) { + return d; + } + } + + audio_module_load_one(name); + QLIST_FOREACH(d, &audio_drivers, next) { + if (strcmp(name, d->name) == 0) { + return d; + } + } + + return NULL; +} + +static QTAILQ_HEAD(AudioStateHead, AudioState) audio_states = + QTAILQ_HEAD_INITIALIZER(audio_states); + +const struct mixeng_volume nominal_volume = { + .mute = 0, +#ifdef FLOAT_MIXENG + .r = 1.0, + .l = 1.0, +#else + .r = 1ULL << 32, + .l = 1ULL << 32, +#endif +}; + +static bool legacy_config = true; + +int audio_bug (const char *funcname, int cond) +{ + if (cond) { + static int shown; + + AUD_log (NULL, "A bug was just triggered in %s\n", funcname); + if (!shown) { + shown = 1; + AUD_log (NULL, "Save all your work and restart without audio\n"); + AUD_log (NULL, "I am sorry\n"); + } + AUD_log (NULL, "Context:\n"); + abort(); + } + + return cond; +} + +static inline int audio_bits_to_index (int bits) +{ + switch (bits) { + case 8: + return 0; + + case 16: + return 1; + + case 32: + return 2; + + default: + audio_bug ("bits_to_index", 1); + AUD_log (NULL, "invalid bits %d\n", bits); + return 0; + } +} + +void *audio_calloc (const char *funcname, int nmemb, size_t size) +{ + int cond; + size_t len; + + len = nmemb * size; + cond = !nmemb || !size; + cond |= nmemb < 0; + cond |= len < size; + + if (audio_bug ("audio_calloc", cond)) { + AUD_log (NULL, "%s passed invalid arguments to audio_calloc\n", + funcname); + AUD_log (NULL, "nmemb=%d size=%zu (len=%zu)\n", nmemb, size, len); + return NULL; + } + + return g_malloc0 (len); +} + +void AUD_vlog (const char *cap, const char *fmt, va_list ap) +{ + if (cap) { + fprintf(stderr, "%s: ", cap); + } + + vfprintf(stderr, fmt, ap); +} + +void AUD_log (const char *cap, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (cap, fmt, ap); + va_end (ap); +} + +static void audio_print_settings (struct audsettings *as) +{ + dolog ("frequency=%d nchannels=%d fmt=", as->freq, as->nchannels); + + switch (as->fmt) { + case AUDIO_FORMAT_S8: + AUD_log (NULL, "S8"); + break; + case AUDIO_FORMAT_U8: + AUD_log (NULL, "U8"); + break; + case AUDIO_FORMAT_S16: + AUD_log (NULL, "S16"); + break; + case AUDIO_FORMAT_U16: + AUD_log (NULL, "U16"); + break; + case AUDIO_FORMAT_S32: + AUD_log (NULL, "S32"); + break; + case AUDIO_FORMAT_U32: + AUD_log (NULL, "U32"); + break; + case AUDIO_FORMAT_F32: + AUD_log (NULL, "F32"); + break; + default: + AUD_log (NULL, "invalid(%d)", as->fmt); + break; + } + + AUD_log (NULL, " endianness="); + switch (as->endianness) { + case 0: + AUD_log (NULL, "little"); + break; + case 1: + AUD_log (NULL, "big"); + break; + default: + AUD_log (NULL, "invalid"); + break; + } + AUD_log (NULL, "\n"); +} + +static int audio_validate_settings (struct audsettings *as) +{ + int invalid; + + invalid = as->nchannels < 1; + invalid |= as->endianness != 0 && as->endianness != 1; + + switch (as->fmt) { + case AUDIO_FORMAT_S8: + case AUDIO_FORMAT_U8: + case AUDIO_FORMAT_S16: + case AUDIO_FORMAT_U16: + case AUDIO_FORMAT_S32: + case AUDIO_FORMAT_U32: + case AUDIO_FORMAT_F32: + break; + default: + invalid = 1; + break; + } + + invalid |= as->freq <= 0; + return invalid ? -1 : 0; +} + +static int audio_pcm_info_eq (struct audio_pcm_info *info, struct audsettings *as) +{ + int bits = 8; + bool is_signed = false, is_float = false; + + switch (as->fmt) { + case AUDIO_FORMAT_S8: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U8: + break; + + case AUDIO_FORMAT_S16: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U16: + bits = 16; + break; + + case AUDIO_FORMAT_F32: + is_float = true; + /* fall through */ + case AUDIO_FORMAT_S32: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U32: + bits = 32; + break; + + default: + abort(); + } + return info->freq == as->freq + && info->nchannels == as->nchannels + && info->is_signed == is_signed + && info->is_float == is_float + && info->bits == bits + && info->swap_endianness == (as->endianness != AUDIO_HOST_ENDIANNESS); +} + +void audio_pcm_init_info (struct audio_pcm_info *info, struct audsettings *as) +{ + int bits = 8, mul; + bool is_signed = false, is_float = false; + + switch (as->fmt) { + case AUDIO_FORMAT_S8: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U8: + mul = 1; + break; + + case AUDIO_FORMAT_S16: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U16: + bits = 16; + mul = 2; + break; + + case AUDIO_FORMAT_F32: + is_float = true; + /* fall through */ + case AUDIO_FORMAT_S32: + is_signed = true; + /* fall through */ + case AUDIO_FORMAT_U32: + bits = 32; + mul = 4; + break; + + default: + abort(); + } + + info->freq = as->freq; + info->bits = bits; + info->is_signed = is_signed; + info->is_float = is_float; + info->nchannels = as->nchannels; + info->bytes_per_frame = as->nchannels * mul; + info->bytes_per_second = info->freq * info->bytes_per_frame; + info->swap_endianness = (as->endianness != AUDIO_HOST_ENDIANNESS); +} + +void audio_pcm_info_clear_buf (struct audio_pcm_info *info, void *buf, int len) +{ + if (!len) { + return; + } + + if (info->is_signed || info->is_float) { + memset(buf, 0x00, len * info->bytes_per_frame); + } else { + switch (info->bits) { + case 8: + memset(buf, 0x80, len * info->bytes_per_frame); + break; + + case 16: + { + int i; + uint16_t *p = buf; + short s = INT16_MAX; + + if (info->swap_endianness) { + s = bswap16 (s); + } + + for (i = 0; i < len * info->nchannels; i++) { + p[i] = s; + } + } + break; + + case 32: + { + int i; + uint32_t *p = buf; + int32_t s = INT32_MAX; + + if (info->swap_endianness) { + s = bswap32 (s); + } + + for (i = 0; i < len * info->nchannels; i++) { + p[i] = s; + } + } + break; + + default: + AUD_log (NULL, "audio_pcm_info_clear_buf: invalid bits %d\n", + info->bits); + break; + } + } +} + +/* + * Capture + */ +static void noop_conv (struct st_sample *dst, const void *src, int samples) +{ + (void) src; + (void) dst; + (void) samples; +} + +static CaptureVoiceOut *audio_pcm_capture_find_specific(AudioState *s, + struct audsettings *as) +{ + CaptureVoiceOut *cap; + + for (cap = s->cap_head.lh_first; cap; cap = cap->entries.le_next) { + if (audio_pcm_info_eq (&cap->hw.info, as)) { + return cap; + } + } + return NULL; +} + +static void audio_notify_capture (CaptureVoiceOut *cap, audcnotification_e cmd) +{ + struct capture_callback *cb; + +#ifdef DEBUG_CAPTURE + dolog ("notification %d sent\n", cmd); +#endif + for (cb = cap->cb_head.lh_first; cb; cb = cb->entries.le_next) { + cb->ops.notify (cb->opaque, cmd); + } +} + +static void audio_capture_maybe_changed (CaptureVoiceOut *cap, int enabled) +{ + if (cap->hw.enabled != enabled) { + audcnotification_e cmd; + cap->hw.enabled = enabled; + cmd = enabled ? AUD_CNOTIFY_ENABLE : AUD_CNOTIFY_DISABLE; + audio_notify_capture (cap, cmd); + } +} + +static void audio_recalc_and_notify_capture (CaptureVoiceOut *cap) +{ + HWVoiceOut *hw = &cap->hw; + SWVoiceOut *sw; + int enabled = 0; + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (sw->active) { + enabled = 1; + break; + } + } + audio_capture_maybe_changed (cap, enabled); +} + +static void audio_detach_capture (HWVoiceOut *hw) +{ + SWVoiceCap *sc = hw->cap_head.lh_first; + + while (sc) { + SWVoiceCap *sc1 = sc->entries.le_next; + SWVoiceOut *sw = &sc->sw; + CaptureVoiceOut *cap = sc->cap; + int was_active = sw->active; + + if (sw->rate) { + st_rate_stop (sw->rate); + sw->rate = NULL; + } + + QLIST_REMOVE (sw, entries); + QLIST_REMOVE (sc, entries); + g_free (sc); + if (was_active) { + /* We have removed soft voice from the capture: + this might have changed the overall status of the capture + since this might have been the only active voice */ + audio_recalc_and_notify_capture (cap); + } + sc = sc1; + } +} + +static int audio_attach_capture (HWVoiceOut *hw) +{ + AudioState *s = hw->s; + CaptureVoiceOut *cap; + + audio_detach_capture (hw); + for (cap = s->cap_head.lh_first; cap; cap = cap->entries.le_next) { + SWVoiceCap *sc; + SWVoiceOut *sw; + HWVoiceOut *hw_cap = &cap->hw; + + sc = g_malloc0(sizeof(*sc)); + + sc->cap = cap; + sw = &sc->sw; + sw->hw = hw_cap; + sw->info = hw->info; + sw->empty = 1; + sw->active = hw->enabled; + sw->conv = noop_conv; + sw->ratio = ((int64_t) hw_cap->info.freq << 32) / sw->info.freq; + sw->vol = nominal_volume; + sw->rate = st_rate_start (sw->info.freq, hw_cap->info.freq); + if (!sw->rate) { + dolog ("Could not start rate conversion for `%s'\n", SW_NAME (sw)); + g_free (sw); + return -1; + } + QLIST_INSERT_HEAD (&hw_cap->sw_head, sw, entries); + QLIST_INSERT_HEAD (&hw->cap_head, sc, entries); +#ifdef DEBUG_CAPTURE + sw->name = g_strdup_printf ("for %p %d,%d,%d", + hw, sw->info.freq, sw->info.bits, + sw->info.nchannels); + dolog ("Added %s active = %d\n", sw->name, sw->active); +#endif + if (sw->active) { + audio_capture_maybe_changed (cap, 1); + } + } + return 0; +} + +/* + * Hard voice (capture) + */ +static size_t audio_pcm_hw_find_min_in (HWVoiceIn *hw) +{ + SWVoiceIn *sw; + size_t m = hw->total_samples_captured; + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (sw->active) { + m = MIN (m, sw->total_hw_samples_acquired); + } + } + return m; +} + +static size_t audio_pcm_hw_get_live_in(HWVoiceIn *hw) +{ + size_t live = hw->total_samples_captured - audio_pcm_hw_find_min_in (hw); + if (audio_bug(__func__, live > hw->conv_buf->size)) { + dolog("live=%zu hw->conv_buf->size=%zu\n", live, hw->conv_buf->size); + return 0; + } + return live; +} + +static void audio_pcm_hw_clip_out(HWVoiceOut *hw, void *pcm_buf, size_t len) +{ + size_t clipped = 0; + size_t pos = hw->mix_buf->pos; + + while (len) { + st_sample *src = hw->mix_buf->samples + pos; + uint8_t *dst = advance(pcm_buf, clipped * hw->info.bytes_per_frame); + size_t samples_till_end_of_buf = hw->mix_buf->size - pos; + size_t samples_to_clip = MIN(len, samples_till_end_of_buf); + + hw->clip(dst, src, samples_to_clip); + + pos = (pos + samples_to_clip) % hw->mix_buf->size; + len -= samples_to_clip; + clipped += samples_to_clip; + } +} + +/* + * Soft voice (capture) + */ +static size_t audio_pcm_sw_get_rpos_in(SWVoiceIn *sw) +{ + HWVoiceIn *hw = sw->hw; + ssize_t live = hw->total_samples_captured - sw->total_hw_samples_acquired; + ssize_t rpos; + + if (audio_bug(__func__, live < 0 || live > hw->conv_buf->size)) { + dolog("live=%zu hw->conv_buf->size=%zu\n", live, hw->conv_buf->size); + return 0; + } + + rpos = hw->conv_buf->pos - live; + if (rpos >= 0) { + return rpos; + } else { + return hw->conv_buf->size + rpos; + } +} + +static size_t audio_pcm_sw_read(SWVoiceIn *sw, void *buf, size_t size) +{ + HWVoiceIn *hw = sw->hw; + size_t samples, live, ret = 0, swlim, isamp, osamp, rpos, total = 0; + struct st_sample *src, *dst = sw->buf; + + rpos = audio_pcm_sw_get_rpos_in(sw) % hw->conv_buf->size; + + live = hw->total_samples_captured - sw->total_hw_samples_acquired; + if (audio_bug(__func__, live > hw->conv_buf->size)) { + dolog("live_in=%zu hw->conv_buf->size=%zu\n", live, hw->conv_buf->size); + return 0; + } + + samples = size / sw->info.bytes_per_frame; + if (!live) { + return 0; + } + + swlim = (live * sw->ratio) >> 32; + swlim = MIN (swlim, samples); + + while (swlim) { + src = hw->conv_buf->samples + rpos; + if (hw->conv_buf->pos > rpos) { + isamp = hw->conv_buf->pos - rpos; + } else { + isamp = hw->conv_buf->size - rpos; + } + + if (!isamp) { + break; + } + osamp = swlim; + + st_rate_flow (sw->rate, src, dst, &isamp, &osamp); + swlim -= osamp; + rpos = (rpos + isamp) % hw->conv_buf->size; + dst += osamp; + ret += osamp; + total += isamp; + } + + if (hw->pcm_ops && !hw->pcm_ops->volume_in) { + mixeng_volume (sw->buf, ret, &sw->vol); + } + + sw->clip (buf, sw->buf, ret); + sw->total_hw_samples_acquired += total; + return ret * sw->info.bytes_per_frame; +} + +/* + * Hard voice (playback) + */ +static size_t audio_pcm_hw_find_min_out (HWVoiceOut *hw, int *nb_livep) +{ + SWVoiceOut *sw; + size_t m = SIZE_MAX; + int nb_live = 0; + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (sw->active || !sw->empty) { + m = MIN (m, sw->total_hw_samples_mixed); + nb_live += 1; + } + } + + *nb_livep = nb_live; + return m; +} + +static size_t audio_pcm_hw_get_live_out (HWVoiceOut *hw, int *nb_live) +{ + size_t smin; + int nb_live1; + + smin = audio_pcm_hw_find_min_out (hw, &nb_live1); + if (nb_live) { + *nb_live = nb_live1; + } + + if (nb_live1) { + size_t live = smin; + + if (audio_bug(__func__, live > hw->mix_buf->size)) { + dolog("live=%zu hw->mix_buf->size=%zu\n", live, hw->mix_buf->size); + return 0; + } + return live; + } + return 0; +} + +/* + * Soft voice (playback) + */ +static size_t audio_pcm_sw_write(SWVoiceOut *sw, void *buf, size_t size) +{ + size_t hwsamples, samples, isamp, osamp, wpos, live, dead, left, swlim, blck; + size_t ret = 0, pos = 0, total = 0; + + if (!sw) { + return size; + } + + hwsamples = sw->hw->mix_buf->size; + + live = sw->total_hw_samples_mixed; + if (audio_bug(__func__, live > hwsamples)) { + dolog("live=%zu hw->mix_buf->size=%zu\n", live, hwsamples); + return 0; + } + + if (live == hwsamples) { +#ifdef DEBUG_OUT + dolog ("%s is full %zu\n", sw->name, live); +#endif + return 0; + } + + wpos = (sw->hw->mix_buf->pos + live) % hwsamples; + samples = size / sw->info.bytes_per_frame; + + dead = hwsamples - live; + swlim = ((int64_t) dead << 32) / sw->ratio; + swlim = MIN (swlim, samples); + if (swlim) { + sw->conv (sw->buf, buf, swlim); + + if (sw->hw->pcm_ops && !sw->hw->pcm_ops->volume_out) { + mixeng_volume (sw->buf, swlim, &sw->vol); + } + } + + while (swlim) { + dead = hwsamples - live; + left = hwsamples - wpos; + blck = MIN (dead, left); + if (!blck) { + break; + } + isamp = swlim; + osamp = blck; + st_rate_flow_mix ( + sw->rate, + sw->buf + pos, + sw->hw->mix_buf->samples + wpos, + &isamp, + &osamp + ); + ret += isamp; + swlim -= isamp; + pos += isamp; + live += osamp; + wpos = (wpos + osamp) % hwsamples; + total += osamp; + } + + sw->total_hw_samples_mixed += total; + sw->empty = sw->total_hw_samples_mixed == 0; + +#ifdef DEBUG_OUT + dolog ( + "%s: write size %zu ret %zu total sw %zu\n", + SW_NAME (sw), + size / sw->info.bytes_per_frame, + ret, + sw->total_hw_samples_mixed + ); +#endif + + return ret * sw->info.bytes_per_frame; +} + +#ifdef DEBUG_AUDIO +static void audio_pcm_print_info (const char *cap, struct audio_pcm_info *info) +{ + dolog("%s: bits %d, sign %d, float %d, freq %d, nchan %d\n", + cap, info->bits, info->is_signed, info->is_float, info->freq, + info->nchannels); +} +#endif + +#define DAC +#include "audio_template.h" +#undef DAC +#include "audio_template.h" + +/* + * Timer + */ +static int audio_is_timer_needed(AudioState *s) +{ + HWVoiceIn *hwi = NULL; + HWVoiceOut *hwo = NULL; + + while ((hwo = audio_pcm_hw_find_any_enabled_out(s, hwo))) { + if (!hwo->poll_mode) { + return 1; + } + } + while ((hwi = audio_pcm_hw_find_any_enabled_in(s, hwi))) { + if (!hwi->poll_mode) { + return 1; + } + } + return 0; +} + +static void audio_reset_timer (AudioState *s) +{ + if (audio_is_timer_needed(s)) { + timer_mod_anticipate_ns(s->ts, + qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + s->period_ticks); + if (!s->timer_running) { + s->timer_running = true; + s->timer_last = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + trace_audio_timer_start(s->period_ticks / SCALE_MS); + } + } else { + timer_del(s->ts); + if (s->timer_running) { + s->timer_running = false; + trace_audio_timer_stop(); + } + } +} + +static void audio_timer (void *opaque) +{ + int64_t now, diff; + AudioState *s = opaque; + + now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + diff = now - s->timer_last; + if (diff > s->period_ticks * 3 / 2) { + trace_audio_timer_delayed(diff / SCALE_MS); + } + s->timer_last = now; + + audio_run(s, "timer"); + audio_reset_timer(s); +} + +/* + * Public API + */ +size_t AUD_write(SWVoiceOut *sw, void *buf, size_t size) +{ + HWVoiceOut *hw; + + if (!sw) { + /* XXX: Consider options */ + return size; + } + hw = sw->hw; + + if (!hw->enabled) { + dolog ("Writing to disabled voice %s\n", SW_NAME (sw)); + return 0; + } + + if (audio_get_pdo_out(hw->s->dev)->mixing_engine) { + return audio_pcm_sw_write(sw, buf, size); + } else { + return hw->pcm_ops->write(hw, buf, size); + } +} + +size_t AUD_read(SWVoiceIn *sw, void *buf, size_t size) +{ + HWVoiceIn *hw; + + if (!sw) { + /* XXX: Consider options */ + return size; + } + hw = sw->hw; + + if (!hw->enabled) { + dolog ("Reading from disabled voice %s\n", SW_NAME (sw)); + return 0; + } + + if (audio_get_pdo_in(hw->s->dev)->mixing_engine) { + return audio_pcm_sw_read(sw, buf, size); + } else { + return hw->pcm_ops->read(hw, buf, size); + } +} + +int AUD_get_buffer_size_out(SWVoiceOut *sw) +{ + return sw->hw->samples * sw->hw->info.bytes_per_frame; +} + +void AUD_set_active_out (SWVoiceOut *sw, int on) +{ + HWVoiceOut *hw; + + if (!sw) { + return; + } + + hw = sw->hw; + if (sw->active != on) { + AudioState *s = sw->s; + SWVoiceOut *temp_sw; + SWVoiceCap *sc; + + if (on) { + hw->pending_disable = 0; + if (!hw->enabled) { + hw->enabled = 1; + if (s->vm_running) { + if (hw->pcm_ops->enable_out) { + hw->pcm_ops->enable_out(hw, true); + } + audio_reset_timer (s); + } + } + } else { + if (hw->enabled) { + int nb_active = 0; + + for (temp_sw = hw->sw_head.lh_first; temp_sw; + temp_sw = temp_sw->entries.le_next) { + nb_active += temp_sw->active != 0; + } + + hw->pending_disable = nb_active == 1; + } + } + + for (sc = hw->cap_head.lh_first; sc; sc = sc->entries.le_next) { + sc->sw.active = hw->enabled; + if (hw->enabled) { + audio_capture_maybe_changed (sc->cap, 1); + } + } + sw->active = on; + } +} + +void AUD_set_active_in (SWVoiceIn *sw, int on) +{ + HWVoiceIn *hw; + + if (!sw) { + return; + } + + hw = sw->hw; + if (sw->active != on) { + AudioState *s = sw->s; + SWVoiceIn *temp_sw; + + if (on) { + if (!hw->enabled) { + hw->enabled = 1; + if (s->vm_running) { + if (hw->pcm_ops->enable_in) { + hw->pcm_ops->enable_in(hw, true); + } + audio_reset_timer (s); + } + } + sw->total_hw_samples_acquired = hw->total_samples_captured; + } else { + if (hw->enabled) { + int nb_active = 0; + + for (temp_sw = hw->sw_head.lh_first; temp_sw; + temp_sw = temp_sw->entries.le_next) { + nb_active += temp_sw->active != 0; + } + + if (nb_active == 1) { + hw->enabled = 0; + if (hw->pcm_ops->enable_in) { + hw->pcm_ops->enable_in(hw, false); + } + } + } + } + sw->active = on; + } +} + +static size_t audio_get_avail (SWVoiceIn *sw) +{ + size_t live; + + if (!sw) { + return 0; + } + + live = sw->hw->total_samples_captured - sw->total_hw_samples_acquired; + if (audio_bug(__func__, live > sw->hw->conv_buf->size)) { + dolog("live=%zu sw->hw->conv_buf->size=%zu\n", live, + sw->hw->conv_buf->size); + return 0; + } + + ldebug ( + "%s: get_avail live %zu ret %" PRId64 "\n", + SW_NAME (sw), + live, (((int64_t) live << 32) / sw->ratio) * sw->info.bytes_per_frame + ); + + return (((int64_t) live << 32) / sw->ratio) * sw->info.bytes_per_frame; +} + +static size_t audio_get_free(SWVoiceOut *sw) +{ + size_t live, dead; + + if (!sw) { + return 0; + } + + live = sw->total_hw_samples_mixed; + + if (audio_bug(__func__, live > sw->hw->mix_buf->size)) { + dolog("live=%zu sw->hw->mix_buf->size=%zu\n", live, + sw->hw->mix_buf->size); + return 0; + } + + dead = sw->hw->mix_buf->size - live; + +#ifdef DEBUG_OUT + dolog ("%s: get_free live %zu dead %zu ret %" PRId64 "\n", + SW_NAME (sw), + live, dead, (((int64_t) dead << 32) / sw->ratio) * + sw->info.bytes_per_frame); +#endif + + return (((int64_t) dead << 32) / sw->ratio) * sw->info.bytes_per_frame; +} + +static void audio_capture_mix_and_clear(HWVoiceOut *hw, size_t rpos, + size_t samples) +{ + size_t n; + + if (hw->enabled) { + SWVoiceCap *sc; + + for (sc = hw->cap_head.lh_first; sc; sc = sc->entries.le_next) { + SWVoiceOut *sw = &sc->sw; + int rpos2 = rpos; + + n = samples; + while (n) { + size_t till_end_of_hw = hw->mix_buf->size - rpos2; + size_t to_write = MIN(till_end_of_hw, n); + size_t bytes = to_write * hw->info.bytes_per_frame; + size_t written; + + sw->buf = hw->mix_buf->samples + rpos2; + written = audio_pcm_sw_write (sw, NULL, bytes); + if (written - bytes) { + dolog("Could not mix %zu bytes into a capture " + "buffer, mixed %zu\n", + bytes, written); + break; + } + n -= to_write; + rpos2 = (rpos2 + to_write) % hw->mix_buf->size; + } + } + } + + n = MIN(samples, hw->mix_buf->size - rpos); + mixeng_clear(hw->mix_buf->samples + rpos, n); + mixeng_clear(hw->mix_buf->samples, samples - n); +} + +static size_t audio_pcm_hw_run_out(HWVoiceOut *hw, size_t live) +{ + size_t clipped = 0; + + while (live) { + size_t size = live * hw->info.bytes_per_frame; + size_t decr, proc; + void *buf = hw->pcm_ops->get_buffer_out(hw, &size); + + if (size == 0) { + break; + } + + decr = MIN(size / hw->info.bytes_per_frame, live); + if (buf) { + audio_pcm_hw_clip_out(hw, buf, decr); + } + proc = hw->pcm_ops->put_buffer_out(hw, buf, + decr * hw->info.bytes_per_frame) / + hw->info.bytes_per_frame; + + live -= proc; + clipped += proc; + hw->mix_buf->pos = (hw->mix_buf->pos + proc) % hw->mix_buf->size; + + if (proc == 0 || proc < decr) { + break; + } + } + + if (hw->pcm_ops->run_buffer_out) { + hw->pcm_ops->run_buffer_out(hw); + } + + return clipped; +} + +static void audio_run_out (AudioState *s) +{ + HWVoiceOut *hw = NULL; + SWVoiceOut *sw; + + if (!audio_get_pdo_out(s->dev)->mixing_engine) { + while ((hw = audio_pcm_hw_find_any_enabled_out(s, hw))) { + /* there is exactly 1 sw for each hw with no mixeng */ + sw = hw->sw_head.lh_first; + + if (hw->pending_disable) { + hw->enabled = 0; + hw->pending_disable = 0; + if (hw->pcm_ops->enable_out) { + hw->pcm_ops->enable_out(hw, false); + } + } + + if (sw->active) { + sw->callback.fn(sw->callback.opaque, INT_MAX); + } + } + return; + } + + while ((hw = audio_pcm_hw_find_any_enabled_out(s, hw))) { + size_t played, live, prev_rpos, free; + int nb_live; + + live = audio_pcm_hw_get_live_out (hw, &nb_live); + if (!nb_live) { + live = 0; + } + + if (audio_bug(__func__, live > hw->mix_buf->size)) { + dolog("live=%zu hw->mix_buf->size=%zu\n", live, hw->mix_buf->size); + continue; + } + + if (hw->pending_disable && !nb_live) { + SWVoiceCap *sc; +#ifdef DEBUG_OUT + dolog ("Disabling voice\n"); +#endif + hw->enabled = 0; + hw->pending_disable = 0; + if (hw->pcm_ops->enable_out) { + hw->pcm_ops->enable_out(hw, false); + } + for (sc = hw->cap_head.lh_first; sc; sc = sc->entries.le_next) { + sc->sw.active = 0; + audio_recalc_and_notify_capture (sc->cap); + } + continue; + } + + if (!live) { + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (sw->active) { + free = audio_get_free (sw); + if (free > 0) { + sw->callback.fn (sw->callback.opaque, free); + } + } + } + if (hw->pcm_ops->run_buffer_out) { + hw->pcm_ops->run_buffer_out(hw); + } + continue; + } + + prev_rpos = hw->mix_buf->pos; + played = audio_pcm_hw_run_out(hw, live); + replay_audio_out(&played); + if (audio_bug(__func__, hw->mix_buf->pos >= hw->mix_buf->size)) { + dolog("hw->mix_buf->pos=%zu hw->mix_buf->size=%zu played=%zu\n", + hw->mix_buf->pos, hw->mix_buf->size, played); + hw->mix_buf->pos = 0; + } + +#ifdef DEBUG_OUT + dolog("played=%zu\n", played); +#endif + + if (played) { + hw->ts_helper += played; + audio_capture_mix_and_clear (hw, prev_rpos, played); + } + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (!sw->active && sw->empty) { + continue; + } + + if (audio_bug(__func__, played > sw->total_hw_samples_mixed)) { + dolog("played=%zu sw->total_hw_samples_mixed=%zu\n", + played, sw->total_hw_samples_mixed); + played = sw->total_hw_samples_mixed; + } + + sw->total_hw_samples_mixed -= played; + + if (!sw->total_hw_samples_mixed) { + sw->empty = 1; + } + + if (sw->active) { + free = audio_get_free (sw); + if (free > 0) { + sw->callback.fn (sw->callback.opaque, free); + } + } + } + } +} + +static size_t audio_pcm_hw_run_in(HWVoiceIn *hw, size_t samples) +{ + size_t conv = 0; + STSampleBuffer *conv_buf = hw->conv_buf; + + if (hw->pcm_ops->run_buffer_in) { + hw->pcm_ops->run_buffer_in(hw); + } + + while (samples) { + size_t proc; + size_t size = samples * hw->info.bytes_per_frame; + void *buf = hw->pcm_ops->get_buffer_in(hw, &size); + + assert(size % hw->info.bytes_per_frame == 0); + if (size == 0) { + break; + } + + proc = MIN(size / hw->info.bytes_per_frame, + conv_buf->size - conv_buf->pos); + + hw->conv(conv_buf->samples + conv_buf->pos, buf, proc); + conv_buf->pos = (conv_buf->pos + proc) % conv_buf->size; + + samples -= proc; + conv += proc; + hw->pcm_ops->put_buffer_in(hw, buf, proc * hw->info.bytes_per_frame); + } + + return conv; +} + +static void audio_run_in (AudioState *s) +{ + HWVoiceIn *hw = NULL; + + if (!audio_get_pdo_in(s->dev)->mixing_engine) { + while ((hw = audio_pcm_hw_find_any_enabled_in(s, hw))) { + /* there is exactly 1 sw for each hw with no mixeng */ + SWVoiceIn *sw = hw->sw_head.lh_first; + if (sw->active) { + sw->callback.fn(sw->callback.opaque, INT_MAX); + } + } + return; + } + + while ((hw = audio_pcm_hw_find_any_enabled_in(s, hw))) { + SWVoiceIn *sw; + size_t captured = 0, min; + + if (replay_mode != REPLAY_MODE_PLAY) { + captured = audio_pcm_hw_run_in( + hw, hw->conv_buf->size - audio_pcm_hw_get_live_in(hw)); + } + replay_audio_in(&captured, hw->conv_buf->samples, &hw->conv_buf->pos, + hw->conv_buf->size); + + min = audio_pcm_hw_find_min_in (hw); + hw->total_samples_captured += captured - min; + hw->ts_helper += captured; + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + sw->total_hw_samples_acquired -= min; + + if (sw->active) { + size_t avail; + + avail = audio_get_avail (sw); + if (avail > 0) { + sw->callback.fn (sw->callback.opaque, avail); + } + } + } + } +} + +static void audio_run_capture (AudioState *s) +{ + CaptureVoiceOut *cap; + + for (cap = s->cap_head.lh_first; cap; cap = cap->entries.le_next) { + size_t live, rpos, captured; + HWVoiceOut *hw = &cap->hw; + SWVoiceOut *sw; + + captured = live = audio_pcm_hw_get_live_out (hw, NULL); + rpos = hw->mix_buf->pos; + while (live) { + size_t left = hw->mix_buf->size - rpos; + size_t to_capture = MIN(live, left); + struct st_sample *src; + struct capture_callback *cb; + + src = hw->mix_buf->samples + rpos; + hw->clip (cap->buf, src, to_capture); + mixeng_clear (src, to_capture); + + for (cb = cap->cb_head.lh_first; cb; cb = cb->entries.le_next) { + cb->ops.capture (cb->opaque, cap->buf, + to_capture * hw->info.bytes_per_frame); + } + rpos = (rpos + to_capture) % hw->mix_buf->size; + live -= to_capture; + } + hw->mix_buf->pos = rpos; + + for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { + if (!sw->active && sw->empty) { + continue; + } + + if (audio_bug(__func__, captured > sw->total_hw_samples_mixed)) { + dolog("captured=%zu sw->total_hw_samples_mixed=%zu\n", + captured, sw->total_hw_samples_mixed); + captured = sw->total_hw_samples_mixed; + } + + sw->total_hw_samples_mixed -= captured; + sw->empty = sw->total_hw_samples_mixed == 0; + } + } +} + +void audio_run(AudioState *s, const char *msg) +{ + audio_run_out(s); + audio_run_in(s); + audio_run_capture(s); + +#ifdef DEBUG_POLL + { + static double prevtime; + double currtime; + struct timeval tv; + + if (gettimeofday (&tv, NULL)) { + perror ("audio_run: gettimeofday"); + return; + } + + currtime = tv.tv_sec + tv.tv_usec * 1e-6; + dolog ("Elapsed since last %s: %f\n", msg, currtime - prevtime); + prevtime = currtime; + } +#endif +} + +void audio_generic_run_buffer_in(HWVoiceIn *hw) +{ + if (unlikely(!hw->buf_emul)) { + hw->size_emul = hw->samples * hw->info.bytes_per_frame; + hw->buf_emul = g_malloc(hw->size_emul); + hw->pos_emul = hw->pending_emul = 0; + } + + while (hw->pending_emul < hw->size_emul) { + size_t read_len = MIN(hw->size_emul - hw->pos_emul, + hw->size_emul - hw->pending_emul); + size_t read = hw->pcm_ops->read(hw, hw->buf_emul + hw->pos_emul, + read_len); + hw->pending_emul += read; + hw->pos_emul = (hw->pos_emul + read) % hw->size_emul; + if (read < read_len) { + break; + } + } +} + +void *audio_generic_get_buffer_in(HWVoiceIn *hw, size_t *size) +{ + ssize_t start = (ssize_t)hw->pos_emul - hw->pending_emul; + + if (start < 0) { + start += hw->size_emul; + } + assert(start >= 0 && start < hw->size_emul); + + *size = MIN(*size, hw->pending_emul); + *size = MIN(*size, hw->size_emul - start); + return hw->buf_emul + start; +} + +void audio_generic_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size) +{ + assert(size <= hw->pending_emul); + hw->pending_emul -= size; +} + +void audio_generic_run_buffer_out(HWVoiceOut *hw) +{ + while (hw->pending_emul) { + size_t write_len, written; + ssize_t start = ((ssize_t) hw->pos_emul) - hw->pending_emul; + + if (start < 0) { + start += hw->size_emul; + } + assert(start >= 0 && start < hw->size_emul); + + write_len = MIN(hw->pending_emul, hw->size_emul - start); + + written = hw->pcm_ops->write(hw, hw->buf_emul + start, write_len); + hw->pending_emul -= written; + + if (written < write_len) { + break; + } + } +} + +void *audio_generic_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + if (unlikely(!hw->buf_emul)) { + hw->size_emul = hw->samples * hw->info.bytes_per_frame; + hw->buf_emul = g_malloc(hw->size_emul); + hw->pos_emul = hw->pending_emul = 0; + } + + *size = MIN(hw->size_emul - hw->pending_emul, + hw->size_emul - hw->pos_emul); + return hw->buf_emul + hw->pos_emul; +} + +size_t audio_generic_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size) +{ + assert(buf == hw->buf_emul + hw->pos_emul && + size + hw->pending_emul <= hw->size_emul); + + hw->pending_emul += size; + hw->pos_emul = (hw->pos_emul + size) % hw->size_emul; + + return size; +} + +size_t audio_generic_write(HWVoiceOut *hw, void *buf, size_t size) +{ + size_t total = 0; + + while (total < size) { + size_t dst_size = size - total; + size_t copy_size, proc; + void *dst = hw->pcm_ops->get_buffer_out(hw, &dst_size); + + if (dst_size == 0) { + break; + } + + copy_size = MIN(size - total, dst_size); + if (dst) { + memcpy(dst, (char *)buf + total, copy_size); + } + proc = hw->pcm_ops->put_buffer_out(hw, dst, copy_size); + total += proc; + + if (proc == 0 || proc < copy_size) { + break; + } + } + + if (hw->pcm_ops->run_buffer_out) { + hw->pcm_ops->run_buffer_out(hw); + } + + return total; +} + +size_t audio_generic_read(HWVoiceIn *hw, void *buf, size_t size) +{ + size_t total = 0; + + if (hw->pcm_ops->run_buffer_in) { + hw->pcm_ops->run_buffer_in(hw); + } + + while (total < size) { + size_t src_size = size - total; + void *src = hw->pcm_ops->get_buffer_in(hw, &src_size); + + if (src_size == 0) { + break; + } + + memcpy((char *)buf + total, src, src_size); + hw->pcm_ops->put_buffer_in(hw, src, src_size); + total += src_size; + } + + return total; +} + +static int audio_driver_init(AudioState *s, struct audio_driver *drv, + bool msg, Audiodev *dev) +{ + s->drv_opaque = drv->init(dev); + + if (s->drv_opaque) { + if (!drv->pcm_ops->get_buffer_in) { + drv->pcm_ops->get_buffer_in = audio_generic_get_buffer_in; + drv->pcm_ops->put_buffer_in = audio_generic_put_buffer_in; + } + if (!drv->pcm_ops->get_buffer_out) { + drv->pcm_ops->get_buffer_out = audio_generic_get_buffer_out; + drv->pcm_ops->put_buffer_out = audio_generic_put_buffer_out; + } + + audio_init_nb_voices_out(s, drv); + audio_init_nb_voices_in(s, drv); + s->drv = drv; + return 0; + } else { + if (msg) { + dolog("Could not init `%s' audio driver\n", drv->name); + } + return -1; + } +} + +static void audio_vm_change_state_handler (void *opaque, bool running, + RunState state) +{ + AudioState *s = opaque; + HWVoiceOut *hwo = NULL; + HWVoiceIn *hwi = NULL; + + s->vm_running = running; + while ((hwo = audio_pcm_hw_find_any_enabled_out(s, hwo))) { + if (hwo->pcm_ops->enable_out) { + hwo->pcm_ops->enable_out(hwo, running); + } + } + + while ((hwi = audio_pcm_hw_find_any_enabled_in(s, hwi))) { + if (hwi->pcm_ops->enable_in) { + hwi->pcm_ops->enable_in(hwi, running); + } + } + audio_reset_timer (s); +} + +static void free_audio_state(AudioState *s) +{ + HWVoiceOut *hwo, *hwon; + HWVoiceIn *hwi, *hwin; + + QLIST_FOREACH_SAFE(hwo, &s->hw_head_out, entries, hwon) { + SWVoiceCap *sc; + + if (hwo->enabled && hwo->pcm_ops->enable_out) { + hwo->pcm_ops->enable_out(hwo, false); + } + hwo->pcm_ops->fini_out (hwo); + + for (sc = hwo->cap_head.lh_first; sc; sc = sc->entries.le_next) { + CaptureVoiceOut *cap = sc->cap; + struct capture_callback *cb; + + for (cb = cap->cb_head.lh_first; cb; cb = cb->entries.le_next) { + cb->ops.destroy (cb->opaque); + } + } + QLIST_REMOVE(hwo, entries); + } + + QLIST_FOREACH_SAFE(hwi, &s->hw_head_in, entries, hwin) { + if (hwi->enabled && hwi->pcm_ops->enable_in) { + hwi->pcm_ops->enable_in(hwi, false); + } + hwi->pcm_ops->fini_in (hwi); + QLIST_REMOVE(hwi, entries); + } + + if (s->drv) { + s->drv->fini (s->drv_opaque); + s->drv = NULL; + } + + if (s->dev) { + qapi_free_Audiodev(s->dev); + s->dev = NULL; + } + + if (s->ts) { + timer_free(s->ts); + s->ts = NULL; + } + + g_free(s); +} + +void audio_cleanup(void) +{ + while (!QTAILQ_EMPTY(&audio_states)) { + AudioState *s = QTAILQ_FIRST(&audio_states); + QTAILQ_REMOVE(&audio_states, s, list); + free_audio_state(s); + } +} + +static bool vmstate_audio_needed(void *opaque) +{ + /* + * Never needed, this vmstate only exists in case + * an old qemu sends it to us. + */ + return false; +} + +static const VMStateDescription vmstate_audio = { + .name = "audio", + .version_id = 1, + .minimum_version_id = 1, + .needed = vmstate_audio_needed, + .fields = (VMStateField[]) { + VMSTATE_END_OF_LIST() + } +}; + +static void audio_validate_opts(Audiodev *dev, Error **errp); + +static AudiodevListEntry *audiodev_find( + AudiodevListHead *head, const char *drvname) +{ + AudiodevListEntry *e; + QSIMPLEQ_FOREACH(e, head, next) { + if (strcmp(AudiodevDriver_str(e->dev->driver), drvname) == 0) { + return e; + } + } + + return NULL; +} + +/* + * if we have dev, this function was called because of an -audiodev argument => + * initialize a new state with it + * if dev == NULL => legacy implicit initialization, return the already created + * state or create a new one + */ +static AudioState *audio_init(Audiodev *dev, const char *name) +{ + static bool atexit_registered; + size_t i; + int done = 0; + const char *drvname = NULL; + VMChangeStateEntry *e; + AudioState *s; + struct audio_driver *driver; + /* silence gcc warning about uninitialized variable */ + AudiodevListHead head = QSIMPLEQ_HEAD_INITIALIZER(head); + + if (using_spice) { + /* + * When using spice allow the spice audio driver being picked + * as default. + * + * Temporary hack. Using audio devices without explicit + * audiodev= property is already deprecated. Same goes for + * the -soundhw switch. Once this support gets finally + * removed we can also drop the concept of a default audio + * backend and this can go away. + */ + driver = audio_driver_lookup("spice"); + if (driver) { + driver->can_be_default = 1; + } + } + + if (dev) { + /* -audiodev option */ + legacy_config = false; + drvname = AudiodevDriver_str(dev->driver); + } else if (!QTAILQ_EMPTY(&audio_states)) { + if (!legacy_config) { + dolog("Device %s: audiodev default parameter is deprecated, please " + "specify audiodev=%s\n", name, + QTAILQ_FIRST(&audio_states)->dev->id); + } + return QTAILQ_FIRST(&audio_states); + } else { + /* legacy implicit initialization */ + head = audio_handle_legacy_opts(); + /* + * In case of legacy initialization, all Audiodevs in the list will have + * the same configuration (except the driver), so it doesn't matter which + * one we chose. We need an Audiodev to set up AudioState before we can + * init a driver. Also note that dev at this point is still in the + * list. + */ + dev = QSIMPLEQ_FIRST(&head)->dev; + audio_validate_opts(dev, &error_abort); + } + + s = g_malloc0(sizeof(AudioState)); + s->dev = dev; + + QLIST_INIT (&s->hw_head_out); + QLIST_INIT (&s->hw_head_in); + QLIST_INIT (&s->cap_head); + if (!atexit_registered) { + atexit(audio_cleanup); + atexit_registered = true; + } + QTAILQ_INSERT_TAIL(&audio_states, s, list); + + s->ts = timer_new_ns(QEMU_CLOCK_VIRTUAL, audio_timer, s); + + s->nb_hw_voices_out = audio_get_pdo_out(dev)->voices; + s->nb_hw_voices_in = audio_get_pdo_in(dev)->voices; + + if (s->nb_hw_voices_out <= 0) { + dolog ("Bogus number of playback voices %d, setting to 1\n", + s->nb_hw_voices_out); + s->nb_hw_voices_out = 1; + } + + if (s->nb_hw_voices_in <= 0) { + dolog ("Bogus number of capture voices %d, setting to 0\n", + s->nb_hw_voices_in); + s->nb_hw_voices_in = 0; + } + + if (drvname) { + driver = audio_driver_lookup(drvname); + if (driver) { + done = !audio_driver_init(s, driver, true, dev); + } else { + dolog ("Unknown audio driver `%s'\n", drvname); + } + } else { + for (i = 0; audio_prio_list[i]; i++) { + AudiodevListEntry *e = audiodev_find(&head, audio_prio_list[i]); + driver = audio_driver_lookup(audio_prio_list[i]); + + if (e && driver) { + s->dev = dev = e->dev; + audio_validate_opts(dev, &error_abort); + done = !audio_driver_init(s, driver, false, dev); + if (done) { + e->dev = NULL; + break; + } + } + } + } + audio_free_audiodev_list(&head); + + if (!done) { + driver = audio_driver_lookup("none"); + done = !audio_driver_init(s, driver, false, dev); + assert(done); + dolog("warning: Using timer based audio emulation\n"); + } + + if (dev->timer_period <= 0) { + s->period_ticks = 1; + } else { + s->period_ticks = dev->timer_period * (int64_t)SCALE_US; + } + + e = qemu_add_vm_change_state_handler (audio_vm_change_state_handler, s); + if (!e) { + dolog ("warning: Could not register change state handler\n" + "(Audio can continue looping even after stopping the VM)\n"); + } + + QLIST_INIT (&s->card_head); + vmstate_register (NULL, 0, &vmstate_audio, s); + return s; +} + +void audio_free_audiodev_list(AudiodevListHead *head) +{ + AudiodevListEntry *e; + while ((e = QSIMPLEQ_FIRST(head))) { + QSIMPLEQ_REMOVE_HEAD(head, next); + qapi_free_Audiodev(e->dev); + g_free(e); + } +} + +void AUD_register_card (const char *name, QEMUSoundCard *card) +{ + if (!card->state) { + card->state = audio_init(NULL, name); + } + + card->name = g_strdup (name); + memset (&card->entries, 0, sizeof (card->entries)); + QLIST_INSERT_HEAD(&card->state->card_head, card, entries); +} + +void AUD_remove_card (QEMUSoundCard *card) +{ + QLIST_REMOVE (card, entries); + g_free (card->name); +} + + +CaptureVoiceOut *AUD_add_capture( + AudioState *s, + struct audsettings *as, + struct audio_capture_ops *ops, + void *cb_opaque + ) +{ + CaptureVoiceOut *cap; + struct capture_callback *cb; + + if (!s) { + if (!legacy_config) { + dolog("Capturing without setting an audiodev is deprecated\n"); + } + s = audio_init(NULL, NULL); + } + + if (!audio_get_pdo_out(s->dev)->mixing_engine) { + dolog("Can't capture with mixeng disabled\n"); + return NULL; + } + + if (audio_validate_settings (as)) { + dolog ("Invalid settings were passed when trying to add capture\n"); + audio_print_settings (as); + return NULL; + } + + cb = g_malloc0(sizeof(*cb)); + cb->ops = *ops; + cb->opaque = cb_opaque; + + cap = audio_pcm_capture_find_specific(s, as); + if (cap) { + QLIST_INSERT_HEAD (&cap->cb_head, cb, entries); + return cap; + } else { + HWVoiceOut *hw; + CaptureVoiceOut *cap; + + cap = g_malloc0(sizeof(*cap)); + + hw = &cap->hw; + hw->s = s; + QLIST_INIT (&hw->sw_head); + QLIST_INIT (&cap->cb_head); + + /* XXX find a more elegant way */ + hw->samples = 4096 * 4; + audio_pcm_hw_alloc_resources_out(hw); + + audio_pcm_init_info (&hw->info, as); + + cap->buf = g_malloc0_n(hw->mix_buf->size, hw->info.bytes_per_frame); + + if (hw->info.is_float) { + hw->clip = mixeng_clip_float[hw->info.nchannels == 2]; + } else { + hw->clip = mixeng_clip + [hw->info.nchannels == 2] + [hw->info.is_signed] + [hw->info.swap_endianness] + [audio_bits_to_index(hw->info.bits)]; + } + + QLIST_INSERT_HEAD (&s->cap_head, cap, entries); + QLIST_INSERT_HEAD (&cap->cb_head, cb, entries); + + QLIST_FOREACH(hw, &s->hw_head_out, entries) { + audio_attach_capture (hw); + } + return cap; + } +} + +void AUD_del_capture (CaptureVoiceOut *cap, void *cb_opaque) +{ + struct capture_callback *cb; + + for (cb = cap->cb_head.lh_first; cb; cb = cb->entries.le_next) { + if (cb->opaque == cb_opaque) { + cb->ops.destroy (cb_opaque); + QLIST_REMOVE (cb, entries); + g_free (cb); + + if (!cap->cb_head.lh_first) { + SWVoiceOut *sw = cap->hw.sw_head.lh_first, *sw1; + + while (sw) { + SWVoiceCap *sc = (SWVoiceCap *) sw; +#ifdef DEBUG_CAPTURE + dolog ("freeing %s\n", sw->name); +#endif + + sw1 = sw->entries.le_next; + if (sw->rate) { + st_rate_stop (sw->rate); + sw->rate = NULL; + } + QLIST_REMOVE (sw, entries); + QLIST_REMOVE (sc, entries); + g_free (sc); + sw = sw1; + } + QLIST_REMOVE (cap, entries); + g_free (cap->hw.mix_buf); + g_free (cap->buf); + g_free (cap); + } + return; + } + } +} + +void AUD_set_volume_out (SWVoiceOut *sw, int mute, uint8_t lvol, uint8_t rvol) +{ + Volume vol = { .mute = mute, .channels = 2, .vol = { lvol, rvol } }; + audio_set_volume_out(sw, &vol); +} + +void audio_set_volume_out(SWVoiceOut *sw, Volume *vol) +{ + if (sw) { + HWVoiceOut *hw = sw->hw; + + sw->vol.mute = vol->mute; + sw->vol.l = nominal_volume.l * vol->vol[0] / 255; + sw->vol.r = nominal_volume.l * vol->vol[vol->channels > 1 ? 1 : 0] / + 255; + + if (hw->pcm_ops->volume_out) { + hw->pcm_ops->volume_out(hw, vol); + } + } +} + +void AUD_set_volume_in (SWVoiceIn *sw, int mute, uint8_t lvol, uint8_t rvol) +{ + Volume vol = { .mute = mute, .channels = 2, .vol = { lvol, rvol } }; + audio_set_volume_in(sw, &vol); +} + +void audio_set_volume_in(SWVoiceIn *sw, Volume *vol) +{ + if (sw) { + HWVoiceIn *hw = sw->hw; + + sw->vol.mute = vol->mute; + sw->vol.l = nominal_volume.l * vol->vol[0] / 255; + sw->vol.r = nominal_volume.r * vol->vol[vol->channels > 1 ? 1 : 0] / + 255; + + if (hw->pcm_ops->volume_in) { + hw->pcm_ops->volume_in(hw, vol); + } + } +} + +void audio_create_pdos(Audiodev *dev) +{ + switch (dev->driver) { +#define CASE(DRIVER, driver, pdo_name) \ + case AUDIODEV_DRIVER_##DRIVER: \ + if (!dev->u.driver.has_in) { \ + dev->u.driver.in = g_malloc0( \ + sizeof(Audiodev##pdo_name##PerDirectionOptions)); \ + dev->u.driver.has_in = true; \ + } \ + if (!dev->u.driver.has_out) { \ + dev->u.driver.out = g_malloc0( \ + sizeof(Audiodev##pdo_name##PerDirectionOptions)); \ + dev->u.driver.has_out = true; \ + } \ + break + + CASE(NONE, none, ); + CASE(ALSA, alsa, Alsa); + CASE(COREAUDIO, coreaudio, Coreaudio); + CASE(DSOUND, dsound, ); + CASE(JACK, jack, Jack); + CASE(OSS, oss, Oss); + CASE(PA, pa, Pa); + CASE(SDL, sdl, Sdl); + CASE(SPICE, spice, ); + CASE(WAV, wav, ); + + case AUDIODEV_DRIVER__MAX: + abort(); + }; +} + +static void audio_validate_per_direction_opts( + AudiodevPerDirectionOptions *pdo, Error **errp) +{ + if (!pdo->has_mixing_engine) { + pdo->has_mixing_engine = true; + pdo->mixing_engine = true; + } + if (!pdo->has_fixed_settings) { + pdo->has_fixed_settings = true; + pdo->fixed_settings = pdo->mixing_engine; + } + if (!pdo->fixed_settings && + (pdo->has_frequency || pdo->has_channels || pdo->has_format)) { + error_setg(errp, + "You can't use frequency, channels or format with fixed-settings=off"); + return; + } + if (!pdo->mixing_engine && pdo->fixed_settings) { + error_setg(errp, "You can't use fixed-settings without mixeng"); + return; + } + + if (!pdo->has_frequency) { + pdo->has_frequency = true; + pdo->frequency = 44100; + } + if (!pdo->has_channels) { + pdo->has_channels = true; + pdo->channels = 2; + } + if (!pdo->has_voices) { + pdo->has_voices = true; + pdo->voices = pdo->mixing_engine ? 1 : INT_MAX; + } + if (!pdo->has_format) { + pdo->has_format = true; + pdo->format = AUDIO_FORMAT_S16; + } +} + +static void audio_validate_opts(Audiodev *dev, Error **errp) +{ + Error *err = NULL; + + audio_create_pdos(dev); + + audio_validate_per_direction_opts(audio_get_pdo_in(dev), &err); + if (err) { + error_propagate(errp, err); + return; + } + + audio_validate_per_direction_opts(audio_get_pdo_out(dev), &err); + if (err) { + error_propagate(errp, err); + return; + } + + if (!dev->has_timer_period) { + dev->has_timer_period = true; + dev->timer_period = 10000; /* 100Hz -> 10ms */ + } +} + +void audio_parse_option(const char *opt) +{ + AudiodevListEntry *e; + Audiodev *dev = NULL; + + Visitor *v = qobject_input_visitor_new_str(opt, "driver", &error_fatal); + visit_type_Audiodev(v, NULL, &dev, &error_fatal); + visit_free(v); + + audio_validate_opts(dev, &error_fatal); + + e = g_malloc0(sizeof(AudiodevListEntry)); + e->dev = dev; + QSIMPLEQ_INSERT_TAIL(&audiodevs, e, next); +} + +void audio_init_audiodevs(void) +{ + AudiodevListEntry *e; + + QSIMPLEQ_FOREACH(e, &audiodevs, next) { + audio_init(e->dev, NULL); + } +} + +audsettings audiodev_to_audsettings(AudiodevPerDirectionOptions *pdo) +{ + return (audsettings) { + .freq = pdo->frequency, + .nchannels = pdo->channels, + .fmt = pdo->format, + .endianness = AUDIO_HOST_ENDIANNESS, + }; +} + +int audioformat_bytes_per_sample(AudioFormat fmt) +{ + switch (fmt) { + case AUDIO_FORMAT_U8: + case AUDIO_FORMAT_S8: + return 1; + + case AUDIO_FORMAT_U16: + case AUDIO_FORMAT_S16: + return 2; + + case AUDIO_FORMAT_U32: + case AUDIO_FORMAT_S32: + case AUDIO_FORMAT_F32: + return 4; + + case AUDIO_FORMAT__MAX: + ; + } + abort(); +} + + +/* frames = freq * usec / 1e6 */ +int audio_buffer_frames(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs) +{ + uint64_t usecs = pdo->has_buffer_length ? pdo->buffer_length : def_usecs; + return (as->freq * usecs + 500000) / 1000000; +} + +/* samples = channels * frames = channels * freq * usec / 1e6 */ +int audio_buffer_samples(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs) +{ + return as->nchannels * audio_buffer_frames(pdo, as, def_usecs); +} + +/* + * bytes = bytes_per_sample * samples = + * bytes_per_sample * channels * freq * usec / 1e6 + */ +int audio_buffer_bytes(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs) +{ + return audio_buffer_samples(pdo, as, def_usecs) * + audioformat_bytes_per_sample(as->fmt); +} + +AudioState *audio_state_by_name(const char *name) +{ + AudioState *s; + QTAILQ_FOREACH(s, &audio_states, list) { + assert(s->dev); + if (strcmp(name, s->dev->id) == 0) { + return s; + } + } + return NULL; +} + +const char *audio_get_id(QEMUSoundCard *card) +{ + if (card->state) { + assert(card->state->dev); + return card->state->dev->id; + } else { + return ""; + } +} + +const char *audio_application_name(void) +{ + const char *vm_name; + + vm_name = qemu_get_vm_name(); + return vm_name ? vm_name : "qemu"; +} + +void audio_rate_start(RateCtl *rate) +{ + memset(rate, 0, sizeof(RateCtl)); + rate->start_ticks = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); +} + +size_t audio_rate_get_bytes(struct audio_pcm_info *info, RateCtl *rate, + size_t bytes_avail) +{ + int64_t now; + int64_t ticks; + int64_t bytes; + int64_t samples; + size_t ret; + + now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + ticks = now - rate->start_ticks; + bytes = muldiv64(ticks, info->bytes_per_second, NANOSECONDS_PER_SECOND); + samples = (bytes - rate->bytes_sent) / info->bytes_per_frame; + if (samples < 0 || samples > 65536) { + AUD_log(NULL, "Resetting rate control (%" PRId64 " samples)\n", samples); + audio_rate_start(rate); + samples = 0; + } + + ret = MIN(samples * info->bytes_per_frame, bytes_avail); + rate->bytes_sent += ret; + return ret; +} diff --git a/audio/audio.h b/audio/audio.h new file mode 100644 index 000000000..c8bde536b --- /dev/null +++ b/audio/audio.h @@ -0,0 +1,181 @@ +/* + * QEMU Audio subsystem header + * + * Copyright (c) 2003-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_AUDIO_H +#define QEMU_AUDIO_H + +#include "qemu/queue.h" +#include "qapi/qapi-types-audio.h" +#include "hw/qdev-properties.h" +#include "hw/qdev-properties-system.h" + +typedef void (*audio_callback_fn) (void *opaque, int avail); + +#ifdef HOST_WORDS_BIGENDIAN +#define AUDIO_HOST_ENDIANNESS 1 +#else +#define AUDIO_HOST_ENDIANNESS 0 +#endif + +typedef struct audsettings { + int freq; + int nchannels; + AudioFormat fmt; + int endianness; +} audsettings; + +audsettings audiodev_to_audsettings(AudiodevPerDirectionOptions *pdo); +int audioformat_bytes_per_sample(AudioFormat fmt); +int audio_buffer_frames(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs); +int audio_buffer_samples(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs); +int audio_buffer_bytes(AudiodevPerDirectionOptions *pdo, + audsettings *as, int def_usecs); + +typedef enum { + AUD_CNOTIFY_ENABLE, + AUD_CNOTIFY_DISABLE +} audcnotification_e; + +struct audio_capture_ops { + void (*notify) (void *opaque, audcnotification_e cmd); + void (*capture) (void *opaque, const void *buf, int size); + void (*destroy) (void *opaque); +}; + +struct capture_ops { + void (*info) (void *opaque); + void (*destroy) (void *opaque); +}; + +typedef struct CaptureState { + void *opaque; + struct capture_ops ops; + QLIST_ENTRY (CaptureState) entries; +} CaptureState; + +typedef struct SWVoiceOut SWVoiceOut; +typedef struct CaptureVoiceOut CaptureVoiceOut; +typedef struct SWVoiceIn SWVoiceIn; + +typedef struct AudioState AudioState; +typedef struct QEMUSoundCard { + char *name; + AudioState *state; + QLIST_ENTRY (QEMUSoundCard) entries; +} QEMUSoundCard; + +typedef struct QEMUAudioTimeStamp { + uint64_t old_ts; +} QEMUAudioTimeStamp; + +void AUD_vlog (const char *cap, const char *fmt, va_list ap) GCC_FMT_ATTR(2, 0); +void AUD_log (const char *cap, const char *fmt, ...) GCC_FMT_ATTR(2, 3); + +void AUD_register_card (const char *name, QEMUSoundCard *card); +void AUD_remove_card (QEMUSoundCard *card); +CaptureVoiceOut *AUD_add_capture( + AudioState *s, + struct audsettings *as, + struct audio_capture_ops *ops, + void *opaque + ); +void AUD_del_capture (CaptureVoiceOut *cap, void *cb_opaque); + +SWVoiceOut *AUD_open_out ( + QEMUSoundCard *card, + SWVoiceOut *sw, + const char *name, + void *callback_opaque, + audio_callback_fn callback_fn, + struct audsettings *settings + ); + +void AUD_close_out (QEMUSoundCard *card, SWVoiceOut *sw); +size_t AUD_write (SWVoiceOut *sw, void *pcm_buf, size_t size); +int AUD_get_buffer_size_out (SWVoiceOut *sw); +void AUD_set_active_out (SWVoiceOut *sw, int on); +int AUD_is_active_out (SWVoiceOut *sw); + +void AUD_init_time_stamp_out (SWVoiceOut *sw, QEMUAudioTimeStamp *ts); +uint64_t AUD_get_elapsed_usec_out (SWVoiceOut *sw, QEMUAudioTimeStamp *ts); + +void AUD_set_volume_out (SWVoiceOut *sw, int mute, uint8_t lvol, uint8_t rvol); +void AUD_set_volume_in (SWVoiceIn *sw, int mute, uint8_t lvol, uint8_t rvol); + +#define AUDIO_MAX_CHANNELS 16 +typedef struct Volume { + bool mute; + int channels; + uint8_t vol[AUDIO_MAX_CHANNELS]; +} Volume; + +void audio_set_volume_out(SWVoiceOut *sw, Volume *vol); +void audio_set_volume_in(SWVoiceIn *sw, Volume *vol); + +SWVoiceIn *AUD_open_in ( + QEMUSoundCard *card, + SWVoiceIn *sw, + const char *name, + void *callback_opaque, + audio_callback_fn callback_fn, + struct audsettings *settings + ); + +void AUD_close_in (QEMUSoundCard *card, SWVoiceIn *sw); +size_t AUD_read (SWVoiceIn *sw, void *pcm_buf, size_t size); +void AUD_set_active_in (SWVoiceIn *sw, int on); +int AUD_is_active_in (SWVoiceIn *sw); + +void AUD_init_time_stamp_in (SWVoiceIn *sw, QEMUAudioTimeStamp *ts); +uint64_t AUD_get_elapsed_usec_in (SWVoiceIn *sw, QEMUAudioTimeStamp *ts); + +static inline void *advance (void *p, int incr) +{ + uint8_t *d = p; + return (d + incr); +} + +int wav_start_capture(AudioState *state, CaptureState *s, const char *path, + int freq, int bits, int nchannels); + +void audio_cleanup(void); + +void audio_sample_to_uint64(const void *samples, int pos, + uint64_t *left, uint64_t *right); +void audio_sample_from_uint64(void *samples, int pos, + uint64_t left, uint64_t right); + +void audio_parse_option(const char *opt); +void audio_init_audiodevs(void); +void audio_legacy_help(void); + +AudioState *audio_state_by_name(const char *name); +const char *audio_get_id(QEMUSoundCard *card); + +#define DEFINE_AUDIO_PROPERTIES(_s, _f) \ + DEFINE_PROP_AUDIODEV("audiodev", _s, _f) + +#endif /* QEMU_AUDIO_H */ diff --git a/audio/audio_int.h b/audio/audio_int.h new file mode 100644 index 000000000..6d685e24a --- /dev/null +++ b/audio/audio_int.h @@ -0,0 +1,287 @@ +/* + * QEMU Audio subsystem header + * + * Copyright (c) 2003-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_AUDIO_INT_H +#define QEMU_AUDIO_INT_H + +#ifdef CONFIG_AUDIO_COREAUDIO +#define FLOAT_MIXENG +/* #define RECIPROCAL */ +#endif +#include "mixeng.h" + +struct audio_pcm_ops; + +struct audio_callback { + void *opaque; + audio_callback_fn fn; +}; + +struct audio_pcm_info { + int bits; + bool is_signed; + bool is_float; + int freq; + int nchannels; + int bytes_per_frame; + int bytes_per_second; + int swap_endianness; +}; + +typedef struct AudioState AudioState; +typedef struct SWVoiceCap SWVoiceCap; + +typedef struct STSampleBuffer { + size_t pos, size; + st_sample samples[]; +} STSampleBuffer; + +typedef struct HWVoiceOut { + AudioState *s; + int enabled; + int poll_mode; + int pending_disable; + struct audio_pcm_info info; + + f_sample *clip; + uint64_t ts_helper; + + STSampleBuffer *mix_buf; + void *buf_emul; + size_t pos_emul, pending_emul, size_emul; + + size_t samples; + QLIST_HEAD (sw_out_listhead, SWVoiceOut) sw_head; + QLIST_HEAD (sw_cap_listhead, SWVoiceCap) cap_head; + struct audio_pcm_ops *pcm_ops; + QLIST_ENTRY (HWVoiceOut) entries; +} HWVoiceOut; + +typedef struct HWVoiceIn { + AudioState *s; + int enabled; + int poll_mode; + struct audio_pcm_info info; + + t_sample *conv; + + size_t total_samples_captured; + uint64_t ts_helper; + + STSampleBuffer *conv_buf; + void *buf_emul; + size_t pos_emul, pending_emul, size_emul; + + size_t samples; + QLIST_HEAD (sw_in_listhead, SWVoiceIn) sw_head; + struct audio_pcm_ops *pcm_ops; + QLIST_ENTRY (HWVoiceIn) entries; +} HWVoiceIn; + +struct SWVoiceOut { + QEMUSoundCard *card; + AudioState *s; + struct audio_pcm_info info; + t_sample *conv; + int64_t ratio; + struct st_sample *buf; + void *rate; + size_t total_hw_samples_mixed; + int active; + int empty; + HWVoiceOut *hw; + char *name; + struct mixeng_volume vol; + struct audio_callback callback; + QLIST_ENTRY (SWVoiceOut) entries; +}; + +struct SWVoiceIn { + QEMUSoundCard *card; + AudioState *s; + int active; + struct audio_pcm_info info; + int64_t ratio; + void *rate; + size_t total_hw_samples_acquired; + struct st_sample *buf; + f_sample *clip; + HWVoiceIn *hw; + char *name; + struct mixeng_volume vol; + struct audio_callback callback; + QLIST_ENTRY (SWVoiceIn) entries; +}; + +typedef struct audio_driver audio_driver; +struct audio_driver { + const char *name; + const char *descr; + void *(*init) (Audiodev *); + void (*fini) (void *); + struct audio_pcm_ops *pcm_ops; + int can_be_default; + int max_voices_out; + int max_voices_in; + int voice_size_out; + int voice_size_in; + QLIST_ENTRY(audio_driver) next; +}; + +struct audio_pcm_ops { + int (*init_out)(HWVoiceOut *hw, audsettings *as, void *drv_opaque); + void (*fini_out)(HWVoiceOut *hw); + size_t (*write) (HWVoiceOut *hw, void *buf, size_t size); + void (*run_buffer_out)(HWVoiceOut *hw); + /* + * get a buffer that after later can be passed to put_buffer_out; optional + * returns the buffer, and writes it's size to size (in bytes) + * this is unrelated to the above buffer_size_out function + */ + void *(*get_buffer_out)(HWVoiceOut *hw, size_t *size); + /* + * put back the buffer returned by get_buffer_out; optional + * buf must be equal the pointer returned by get_buffer_out, + * size may be smaller + */ + size_t (*put_buffer_out)(HWVoiceOut *hw, void *buf, size_t size); + void (*enable_out)(HWVoiceOut *hw, bool enable); + void (*volume_out)(HWVoiceOut *hw, Volume *vol); + + int (*init_in) (HWVoiceIn *hw, audsettings *as, void *drv_opaque); + void (*fini_in) (HWVoiceIn *hw); + size_t (*read) (HWVoiceIn *hw, void *buf, size_t size); + void (*run_buffer_in)(HWVoiceIn *hw); + void *(*get_buffer_in)(HWVoiceIn *hw, size_t *size); + void (*put_buffer_in)(HWVoiceIn *hw, void *buf, size_t size); + void (*enable_in)(HWVoiceIn *hw, bool enable); + void (*volume_in)(HWVoiceIn *hw, Volume *vol); +}; + +void audio_generic_run_buffer_in(HWVoiceIn *hw); +void *audio_generic_get_buffer_in(HWVoiceIn *hw, size_t *size); +void audio_generic_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size); +void audio_generic_run_buffer_out(HWVoiceOut *hw); +void *audio_generic_get_buffer_out(HWVoiceOut *hw, size_t *size); +size_t audio_generic_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size); +size_t audio_generic_write(HWVoiceOut *hw, void *buf, size_t size); +size_t audio_generic_read(HWVoiceIn *hw, void *buf, size_t size); + +struct capture_callback { + struct audio_capture_ops ops; + void *opaque; + QLIST_ENTRY (capture_callback) entries; +}; + +struct CaptureVoiceOut { + HWVoiceOut hw; + void *buf; + QLIST_HEAD (cb_listhead, capture_callback) cb_head; + QLIST_ENTRY (CaptureVoiceOut) entries; +}; + +struct SWVoiceCap { + SWVoiceOut sw; + CaptureVoiceOut *cap; + QLIST_ENTRY (SWVoiceCap) entries; +}; + +typedef struct AudioState { + struct audio_driver *drv; + Audiodev *dev; + void *drv_opaque; + + QEMUTimer *ts; + QLIST_HEAD (card_listhead, QEMUSoundCard) card_head; + QLIST_HEAD (hw_in_listhead, HWVoiceIn) hw_head_in; + QLIST_HEAD (hw_out_listhead, HWVoiceOut) hw_head_out; + QLIST_HEAD (cap_listhead, CaptureVoiceOut) cap_head; + int nb_hw_voices_out; + int nb_hw_voices_in; + int vm_running; + int64_t period_ticks; + + bool timer_running; + uint64_t timer_last; + + QTAILQ_ENTRY(AudioState) list; +} AudioState; + +extern const struct mixeng_volume nominal_volume; + +extern const char *audio_prio_list[]; + +void audio_driver_register(audio_driver *drv); +audio_driver *audio_driver_lookup(const char *name); + +void audio_pcm_init_info (struct audio_pcm_info *info, struct audsettings *as); +void audio_pcm_info_clear_buf (struct audio_pcm_info *info, void *buf, int len); + +int audio_bug (const char *funcname, int cond); +void *audio_calloc (const char *funcname, int nmemb, size_t size); + +void audio_run(AudioState *s, const char *msg); + +const char *audio_application_name(void); + +typedef struct RateCtl { + int64_t start_ticks; + int64_t bytes_sent; +} RateCtl; + +void audio_rate_start(RateCtl *rate); +size_t audio_rate_get_bytes(struct audio_pcm_info *info, RateCtl *rate, + size_t bytes_avail); + +static inline size_t audio_ring_dist(size_t dst, size_t src, size_t len) +{ + return (dst >= src) ? (dst - src) : (len - src + dst); +} + +#define dolog(fmt, ...) AUD_log(AUDIO_CAP, fmt, ## __VA_ARGS__) + +#ifdef DEBUG +#define ldebug(fmt, ...) AUD_log(AUDIO_CAP, fmt, ## __VA_ARGS__) +#else +#define ldebug(fmt, ...) (void)0 +#endif + +#define AUDIO_STRINGIFY_(n) #n +#define AUDIO_STRINGIFY(n) AUDIO_STRINGIFY_(n) + +typedef struct AudiodevListEntry { + Audiodev *dev; + QSIMPLEQ_ENTRY(AudiodevListEntry) next; +} AudiodevListEntry; + +typedef QSIMPLEQ_HEAD(, AudiodevListEntry) AudiodevListHead; +AudiodevListHead audio_handle_legacy_opts(void); + +void audio_free_audiodev_list(AudiodevListHead *head); + +void audio_create_pdos(Audiodev *dev); +AudiodevPerDirectionOptions *audio_get_pdo_in(Audiodev *dev); +AudiodevPerDirectionOptions *audio_get_pdo_out(Audiodev *dev); + +#endif /* QEMU_AUDIO_INT_H */ diff --git a/audio/audio_legacy.c b/audio/audio_legacy.c new file mode 100644 index 000000000..0fe827b05 --- /dev/null +++ b/audio/audio_legacy.c @@ -0,0 +1,555 @@ +/* + * QEMU Audio subsystem: legacy configuration handling + * + * Copyright (c) 2015-2019 Zoltán Kővágó <DirtY.iCE.hu@gmail.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "audio.h" +#include "audio_int.h" +#include "qemu/cutils.h" +#include "qemu/timer.h" +#include "qapi/error.h" +#include "qapi/qapi-visit-audio.h" +#include "qapi/visitor-impl.h" + +#define AUDIO_CAP "audio-legacy" +#include "audio_int.h" + +static uint32_t toui32(const char *str) +{ + unsigned long long ret; + if (parse_uint_full(str, &ret, 10) || ret > UINT32_MAX) { + dolog("Invalid integer value `%s'\n", str); + exit(1); + } + return ret; +} + +/* helper functions to convert env variables */ +static void get_bool(const char *env, bool *dst, bool *has_dst) +{ + const char *val = getenv(env); + if (val) { + *dst = toui32(val) != 0; + *has_dst = true; + } +} + +static void get_int(const char *env, uint32_t *dst, bool *has_dst) +{ + const char *val = getenv(env); + if (val) { + *dst = toui32(val); + *has_dst = true; + } +} + +static void get_str(const char *env, char **dst, bool *has_dst) +{ + const char *val = getenv(env); + if (val) { + if (*has_dst) { + g_free(*dst); + } + *dst = g_strdup(val); + *has_dst = true; + } +} + +static void get_fmt(const char *env, AudioFormat *dst, bool *has_dst) +{ + const char *val = getenv(env); + if (val) { + size_t i; + for (i = 0; AudioFormat_lookup.size; ++i) { + if (strcasecmp(val, AudioFormat_lookup.array[i]) == 0) { + *dst = i; + *has_dst = true; + return; + } + } + + dolog("Invalid audio format `%s'\n", val); + exit(1); + } +} + + +static void get_millis_to_usecs(const char *env, uint32_t *dst, bool *has_dst) +{ + const char *val = getenv(env); + if (val) { + *dst = toui32(val) * 1000; + *has_dst = true; + } +} + +static uint32_t frames_to_usecs(uint32_t frames, + AudiodevPerDirectionOptions *pdo) +{ + uint32_t freq = pdo->has_frequency ? pdo->frequency : 44100; + return (frames * 1000000 + freq / 2) / freq; +} + + +static void get_frames_to_usecs(const char *env, uint32_t *dst, bool *has_dst, + AudiodevPerDirectionOptions *pdo) +{ + const char *val = getenv(env); + if (val) { + *dst = frames_to_usecs(toui32(val), pdo); + *has_dst = true; + } +} + +static uint32_t samples_to_usecs(uint32_t samples, + AudiodevPerDirectionOptions *pdo) +{ + uint32_t channels = pdo->has_channels ? pdo->channels : 2; + return frames_to_usecs(samples / channels, pdo); +} + +static void get_samples_to_usecs(const char *env, uint32_t *dst, bool *has_dst, + AudiodevPerDirectionOptions *pdo) +{ + const char *val = getenv(env); + if (val) { + *dst = samples_to_usecs(toui32(val), pdo); + *has_dst = true; + } +} + +static uint32_t bytes_to_usecs(uint32_t bytes, AudiodevPerDirectionOptions *pdo) +{ + AudioFormat fmt = pdo->has_format ? pdo->format : AUDIO_FORMAT_S16; + uint32_t bytes_per_sample = audioformat_bytes_per_sample(fmt); + return samples_to_usecs(bytes / bytes_per_sample, pdo); +} + +static void get_bytes_to_usecs(const char *env, uint32_t *dst, bool *has_dst, + AudiodevPerDirectionOptions *pdo) +{ + const char *val = getenv(env); + if (val) { + *dst = bytes_to_usecs(toui32(val), pdo); + *has_dst = true; + } +} + +/* backend specific functions */ +/* ALSA */ +static void handle_alsa_per_direction( + AudiodevAlsaPerDirectionOptions *apdo, const char *prefix) +{ + char buf[64]; + size_t len = strlen(prefix); + bool size_in_usecs = false; + bool dummy; + + memcpy(buf, prefix, len); + strcpy(buf + len, "TRY_POLL"); + get_bool(buf, &apdo->try_poll, &apdo->has_try_poll); + + strcpy(buf + len, "DEV"); + get_str(buf, &apdo->dev, &apdo->has_dev); + + strcpy(buf + len, "SIZE_IN_USEC"); + get_bool(buf, &size_in_usecs, &dummy); + + strcpy(buf + len, "PERIOD_SIZE"); + get_int(buf, &apdo->period_length, &apdo->has_period_length); + if (apdo->has_period_length && !size_in_usecs) { + apdo->period_length = frames_to_usecs( + apdo->period_length, + qapi_AudiodevAlsaPerDirectionOptions_base(apdo)); + } + + strcpy(buf + len, "BUFFER_SIZE"); + get_int(buf, &apdo->buffer_length, &apdo->has_buffer_length); + if (apdo->has_buffer_length && !size_in_usecs) { + apdo->buffer_length = frames_to_usecs( + apdo->buffer_length, + qapi_AudiodevAlsaPerDirectionOptions_base(apdo)); + } +} + +static void handle_alsa(Audiodev *dev) +{ + AudiodevAlsaOptions *aopt = &dev->u.alsa; + handle_alsa_per_direction(aopt->in, "QEMU_ALSA_ADC_"); + handle_alsa_per_direction(aopt->out, "QEMU_ALSA_DAC_"); + + get_millis_to_usecs("QEMU_ALSA_THRESHOLD", + &aopt->threshold, &aopt->has_threshold); +} + +/* coreaudio */ +static void handle_coreaudio(Audiodev *dev) +{ + get_frames_to_usecs( + "QEMU_COREAUDIO_BUFFER_SIZE", + &dev->u.coreaudio.out->buffer_length, + &dev->u.coreaudio.out->has_buffer_length, + qapi_AudiodevCoreaudioPerDirectionOptions_base(dev->u.coreaudio.out)); + get_int("QEMU_COREAUDIO_BUFFER_COUNT", + &dev->u.coreaudio.out->buffer_count, + &dev->u.coreaudio.out->has_buffer_count); +} + +/* dsound */ +static void handle_dsound(Audiodev *dev) +{ + get_millis_to_usecs("QEMU_DSOUND_LATENCY_MILLIS", + &dev->u.dsound.latency, &dev->u.dsound.has_latency); + get_bytes_to_usecs("QEMU_DSOUND_BUFSIZE_OUT", + &dev->u.dsound.out->buffer_length, + &dev->u.dsound.out->has_buffer_length, + dev->u.dsound.out); + get_bytes_to_usecs("QEMU_DSOUND_BUFSIZE_IN", + &dev->u.dsound.in->buffer_length, + &dev->u.dsound.in->has_buffer_length, + dev->u.dsound.in); +} + +/* OSS */ +static void handle_oss_per_direction( + AudiodevOssPerDirectionOptions *opdo, const char *try_poll_env, + const char *dev_env) +{ + get_bool(try_poll_env, &opdo->try_poll, &opdo->has_try_poll); + get_str(dev_env, &opdo->dev, &opdo->has_dev); + + get_bytes_to_usecs("QEMU_OSS_FRAGSIZE", + &opdo->buffer_length, &opdo->has_buffer_length, + qapi_AudiodevOssPerDirectionOptions_base(opdo)); + get_int("QEMU_OSS_NFRAGS", &opdo->buffer_count, + &opdo->has_buffer_count); +} + +static void handle_oss(Audiodev *dev) +{ + AudiodevOssOptions *oopt = &dev->u.oss; + handle_oss_per_direction(oopt->in, "QEMU_AUDIO_ADC_TRY_POLL", + "QEMU_OSS_ADC_DEV"); + handle_oss_per_direction(oopt->out, "QEMU_AUDIO_DAC_TRY_POLL", + "QEMU_OSS_DAC_DEV"); + + get_bool("QEMU_OSS_MMAP", &oopt->try_mmap, &oopt->has_try_mmap); + get_bool("QEMU_OSS_EXCLUSIVE", &oopt->exclusive, &oopt->has_exclusive); + get_int("QEMU_OSS_POLICY", &oopt->dsp_policy, &oopt->has_dsp_policy); +} + +/* pulseaudio */ +static void handle_pa_per_direction( + AudiodevPaPerDirectionOptions *ppdo, const char *env) +{ + get_str(env, &ppdo->name, &ppdo->has_name); +} + +static void handle_pa(Audiodev *dev) +{ + handle_pa_per_direction(dev->u.pa.in, "QEMU_PA_SOURCE"); + handle_pa_per_direction(dev->u.pa.out, "QEMU_PA_SINK"); + + get_samples_to_usecs( + "QEMU_PA_SAMPLES", &dev->u.pa.in->buffer_length, + &dev->u.pa.in->has_buffer_length, + qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.in)); + get_samples_to_usecs( + "QEMU_PA_SAMPLES", &dev->u.pa.out->buffer_length, + &dev->u.pa.out->has_buffer_length, + qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.out)); + + get_str("QEMU_PA_SERVER", &dev->u.pa.server, &dev->u.pa.has_server); +} + +/* SDL */ +static void handle_sdl(Audiodev *dev) +{ + /* SDL is output only */ + get_samples_to_usecs("QEMU_SDL_SAMPLES", &dev->u.sdl.out->buffer_length, + &dev->u.sdl.out->has_buffer_length, + qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.out)); +} + +/* wav */ +static void handle_wav(Audiodev *dev) +{ + get_int("QEMU_WAV_FREQUENCY", + &dev->u.wav.out->frequency, &dev->u.wav.out->has_frequency); + get_fmt("QEMU_WAV_FORMAT", &dev->u.wav.out->format, + &dev->u.wav.out->has_format); + get_int("QEMU_WAV_DAC_FIXED_CHANNELS", + &dev->u.wav.out->channels, &dev->u.wav.out->has_channels); + get_str("QEMU_WAV_PATH", &dev->u.wav.path, &dev->u.wav.has_path); +} + +/* general */ +static void handle_per_direction( + AudiodevPerDirectionOptions *pdo, const char *prefix) +{ + char buf[64]; + size_t len = strlen(prefix); + + memcpy(buf, prefix, len); + strcpy(buf + len, "FIXED_SETTINGS"); + get_bool(buf, &pdo->fixed_settings, &pdo->has_fixed_settings); + + strcpy(buf + len, "FIXED_FREQ"); + get_int(buf, &pdo->frequency, &pdo->has_frequency); + + strcpy(buf + len, "FIXED_FMT"); + get_fmt(buf, &pdo->format, &pdo->has_format); + + strcpy(buf + len, "FIXED_CHANNELS"); + get_int(buf, &pdo->channels, &pdo->has_channels); + + strcpy(buf + len, "VOICES"); + get_int(buf, &pdo->voices, &pdo->has_voices); +} + +static AudiodevListEntry *legacy_opt(const char *drvname) +{ + AudiodevListEntry *e = g_malloc0(sizeof(AudiodevListEntry)); + e->dev = g_malloc0(sizeof(Audiodev)); + e->dev->id = g_strdup(drvname); + e->dev->driver = qapi_enum_parse( + &AudiodevDriver_lookup, drvname, -1, &error_abort); + + audio_create_pdos(e->dev); + + handle_per_direction(audio_get_pdo_in(e->dev), "QEMU_AUDIO_ADC_"); + handle_per_direction(audio_get_pdo_out(e->dev), "QEMU_AUDIO_DAC_"); + + /* Original description: Timer period in HZ (0 - use lowest possible) */ + get_int("QEMU_AUDIO_TIMER_PERIOD", + &e->dev->timer_period, &e->dev->has_timer_period); + if (e->dev->has_timer_period && e->dev->timer_period) { + e->dev->timer_period = NANOSECONDS_PER_SECOND / 1000 / + e->dev->timer_period; + } + + switch (e->dev->driver) { + case AUDIODEV_DRIVER_ALSA: + handle_alsa(e->dev); + break; + + case AUDIODEV_DRIVER_COREAUDIO: + handle_coreaudio(e->dev); + break; + + case AUDIODEV_DRIVER_DSOUND: + handle_dsound(e->dev); + break; + + case AUDIODEV_DRIVER_OSS: + handle_oss(e->dev); + break; + + case AUDIODEV_DRIVER_PA: + handle_pa(e->dev); + break; + + case AUDIODEV_DRIVER_SDL: + handle_sdl(e->dev); + break; + + case AUDIODEV_DRIVER_WAV: + handle_wav(e->dev); + break; + + default: + break; + } + + return e; +} + +AudiodevListHead audio_handle_legacy_opts(void) +{ + const char *drvname = getenv("QEMU_AUDIO_DRV"); + AudiodevListHead head = QSIMPLEQ_HEAD_INITIALIZER(head); + + if (drvname) { + AudiodevListEntry *e; + audio_driver *driver = audio_driver_lookup(drvname); + if (!driver) { + dolog("Unknown audio driver `%s'\n", drvname); + exit(1); + } + e = legacy_opt(drvname); + QSIMPLEQ_INSERT_TAIL(&head, e, next); + } else { + for (int i = 0; audio_prio_list[i]; i++) { + audio_driver *driver = audio_driver_lookup(audio_prio_list[i]); + if (driver && driver->can_be_default) { + AudiodevListEntry *e = legacy_opt(driver->name); + QSIMPLEQ_INSERT_TAIL(&head, e, next); + } + } + if (QSIMPLEQ_EMPTY(&head)) { + dolog("Internal error: no default audio driver available\n"); + exit(1); + } + } + + return head; +} + +/* visitor to print -audiodev option */ +typedef struct { + Visitor visitor; + + bool comma; + GList *path; +} LegacyPrintVisitor; + +static bool lv_start_struct(Visitor *v, const char *name, void **obj, + size_t size, Error **errp) +{ + LegacyPrintVisitor *lv = (LegacyPrintVisitor *) v; + lv->path = g_list_append(lv->path, g_strdup(name)); + return true; +} + +static void lv_end_struct(Visitor *v, void **obj) +{ + LegacyPrintVisitor *lv = (LegacyPrintVisitor *) v; + lv->path = g_list_delete_link(lv->path, g_list_last(lv->path)); +} + +static void lv_print_key(Visitor *v, const char *name) +{ + GList *e; + LegacyPrintVisitor *lv = (LegacyPrintVisitor *) v; + if (lv->comma) { + putchar(','); + } else { + lv->comma = true; + } + + for (e = lv->path; e; e = e->next) { + if (e->data) { + printf("%s.", (const char *) e->data); + } + } + + printf("%s=", name); +} + +static bool lv_type_int64(Visitor *v, const char *name, int64_t *obj, + Error **errp) +{ + lv_print_key(v, name); + printf("%" PRIi64, *obj); + return true; +} + +static bool lv_type_uint64(Visitor *v, const char *name, uint64_t *obj, + Error **errp) +{ + lv_print_key(v, name); + printf("%" PRIu64, *obj); + return true; +} + +static bool lv_type_bool(Visitor *v, const char *name, bool *obj, Error **errp) +{ + lv_print_key(v, name); + printf("%s", *obj ? "on" : "off"); + return true; +} + +static bool lv_type_str(Visitor *v, const char *name, char **obj, Error **errp) +{ + const char *str = *obj; + lv_print_key(v, name); + + while (*str) { + if (*str == ',') { + putchar(','); + } + putchar(*str++); + } + return true; +} + +static void lv_complete(Visitor *v, void *opaque) +{ + LegacyPrintVisitor *lv = (LegacyPrintVisitor *) v; + assert(lv->path == NULL); +} + +static void lv_free(Visitor *v) +{ + LegacyPrintVisitor *lv = (LegacyPrintVisitor *) v; + + g_list_free_full(lv->path, g_free); + g_free(lv); +} + +static Visitor *legacy_visitor_new(void) +{ + LegacyPrintVisitor *lv = g_malloc0(sizeof(LegacyPrintVisitor)); + + lv->visitor.start_struct = lv_start_struct; + lv->visitor.end_struct = lv_end_struct; + /* lists not supported */ + lv->visitor.type_int64 = lv_type_int64; + lv->visitor.type_uint64 = lv_type_uint64; + lv->visitor.type_bool = lv_type_bool; + lv->visitor.type_str = lv_type_str; + + lv->visitor.type = VISITOR_OUTPUT; + lv->visitor.complete = lv_complete; + lv->visitor.free = lv_free; + + return &lv->visitor; +} + +void audio_legacy_help(void) +{ + AudiodevListHead head; + AudiodevListEntry *e; + + printf("Environment variable based configuration deprecated.\n"); + printf("Please use the new -audiodev option.\n"); + + head = audio_handle_legacy_opts(); + printf("\nEquivalent -audiodev to your current environment variables:\n"); + if (!getenv("QEMU_AUDIO_DRV")) { + printf("(Since you didn't specify QEMU_AUDIO_DRV, I'll list all " + "possibilities)\n"); + } + + QSIMPLEQ_FOREACH(e, &head, next) { + Visitor *v; + Audiodev *dev = e->dev; + printf("-audiodev "); + + v = legacy_visitor_new(); + visit_type_Audiodev(v, NULL, &dev, &error_abort); + visit_free(v); + + printf("\n"); + } + audio_free_audiodev_list(&head); +} diff --git a/audio/audio_template.h b/audio/audio_template.h new file mode 100644 index 000000000..c6714946a --- /dev/null +++ b/audio/audio_template.h @@ -0,0 +1,568 @@ +/* + * QEMU Audio subsystem header + * + * Copyright (c) 2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifdef DAC +#define NAME "playback" +#define HWBUF hw->mix_buf +#define TYPE out +#define HW HWVoiceOut +#define SW SWVoiceOut +#else +#define NAME "capture" +#define TYPE in +#define HW HWVoiceIn +#define SW SWVoiceIn +#define HWBUF hw->conv_buf +#endif + +static void glue(audio_init_nb_voices_, TYPE)(AudioState *s, + struct audio_driver *drv) +{ + int max_voices = glue (drv->max_voices_, TYPE); + int voice_size = glue (drv->voice_size_, TYPE); + + if (glue (s->nb_hw_voices_, TYPE) > max_voices) { + if (!max_voices) { +#ifdef DAC + dolog ("Driver `%s' does not support " NAME "\n", drv->name); +#endif + } else { + dolog ("Driver `%s' does not support %d " NAME " voices, max %d\n", + drv->name, + glue (s->nb_hw_voices_, TYPE), + max_voices); + } + glue (s->nb_hw_voices_, TYPE) = max_voices; + } + + if (audio_bug(__func__, !voice_size && max_voices)) { + dolog ("drv=`%s' voice_size=0 max_voices=%d\n", + drv->name, max_voices); + glue (s->nb_hw_voices_, TYPE) = 0; + } + + if (audio_bug(__func__, voice_size && !max_voices)) { + dolog ("drv=`%s' voice_size=%d max_voices=0\n", + drv->name, voice_size); + } +} + +static void glue (audio_pcm_hw_free_resources_, TYPE) (HW *hw) +{ + g_free(hw->buf_emul); + g_free (HWBUF); + HWBUF = NULL; +} + +static void glue(audio_pcm_hw_alloc_resources_, TYPE)(HW *hw) +{ + if (glue(audio_get_pdo_, TYPE)(hw->s->dev)->mixing_engine) { + size_t samples = hw->samples; + if (audio_bug(__func__, samples == 0)) { + dolog("Attempted to allocate empty buffer\n"); + } + + HWBUF = g_malloc0(sizeof(STSampleBuffer) + sizeof(st_sample) * samples); + HWBUF->size = samples; + } else { + HWBUF = NULL; + } +} + +static void glue (audio_pcm_sw_free_resources_, TYPE) (SW *sw) +{ + g_free (sw->buf); + + if (sw->rate) { + st_rate_stop (sw->rate); + } + + sw->buf = NULL; + sw->rate = NULL; +} + +static int glue (audio_pcm_sw_alloc_resources_, TYPE) (SW *sw) +{ + int samples; + + if (!glue(audio_get_pdo_, TYPE)(sw->s->dev)->mixing_engine) { + return 0; + } + + samples = ((int64_t) sw->HWBUF->size << 32) / sw->ratio; + + sw->buf = audio_calloc(__func__, samples, sizeof(struct st_sample)); + if (!sw->buf) { + dolog ("Could not allocate buffer for `%s' (%d samples)\n", + SW_NAME (sw), samples); + return -1; + } + +#ifdef DAC + sw->rate = st_rate_start (sw->info.freq, sw->hw->info.freq); +#else + sw->rate = st_rate_start (sw->hw->info.freq, sw->info.freq); +#endif + if (!sw->rate) { + g_free (sw->buf); + sw->buf = NULL; + return -1; + } + return 0; +} + +static int glue (audio_pcm_sw_init_, TYPE) ( + SW *sw, + HW *hw, + const char *name, + struct audsettings *as + ) +{ + int err; + + audio_pcm_init_info (&sw->info, as); + sw->hw = hw; + sw->active = 0; +#ifdef DAC + sw->ratio = ((int64_t) sw->hw->info.freq << 32) / sw->info.freq; + sw->total_hw_samples_mixed = 0; + sw->empty = 1; +#else + sw->ratio = ((int64_t) sw->info.freq << 32) / sw->hw->info.freq; +#endif + + if (sw->info.is_float) { +#ifdef DAC + sw->conv = mixeng_conv_float[sw->info.nchannels == 2]; +#else + sw->clip = mixeng_clip_float[sw->info.nchannels == 2]; +#endif + } else { +#ifdef DAC + sw->conv = mixeng_conv +#else + sw->clip = mixeng_clip +#endif + [sw->info.nchannels == 2] + [sw->info.is_signed] + [sw->info.swap_endianness] + [audio_bits_to_index(sw->info.bits)]; + } + + sw->name = g_strdup (name); + err = glue (audio_pcm_sw_alloc_resources_, TYPE) (sw); + if (err) { + g_free (sw->name); + sw->name = NULL; + } + return err; +} + +static void glue (audio_pcm_sw_fini_, TYPE) (SW *sw) +{ + glue (audio_pcm_sw_free_resources_, TYPE) (sw); + g_free (sw->name); + sw->name = NULL; +} + +static void glue (audio_pcm_hw_add_sw_, TYPE) (HW *hw, SW *sw) +{ + QLIST_INSERT_HEAD (&hw->sw_head, sw, entries); +} + +static void glue (audio_pcm_hw_del_sw_, TYPE) (SW *sw) +{ + QLIST_REMOVE (sw, entries); +} + +static void glue (audio_pcm_hw_gc_, TYPE) (HW **hwp) +{ + HW *hw = *hwp; + AudioState *s = hw->s; + + if (!hw->sw_head.lh_first) { +#ifdef DAC + audio_detach_capture(hw); +#endif + QLIST_REMOVE(hw, entries); + glue(hw->pcm_ops->fini_, TYPE) (hw); + glue(s->nb_hw_voices_, TYPE) += 1; + glue(audio_pcm_hw_free_resources_ , TYPE) (hw); + g_free(hw); + *hwp = NULL; + } +} + +static HW *glue(audio_pcm_hw_find_any_, TYPE)(AudioState *s, HW *hw) +{ + return hw ? hw->entries.le_next : glue (s->hw_head_, TYPE).lh_first; +} + +static HW *glue(audio_pcm_hw_find_any_enabled_, TYPE)(AudioState *s, HW *hw) +{ + while ((hw = glue(audio_pcm_hw_find_any_, TYPE)(s, hw))) { + if (hw->enabled) { + return hw; + } + } + return NULL; +} + +static HW *glue(audio_pcm_hw_find_specific_, TYPE)(AudioState *s, HW *hw, + struct audsettings *as) +{ + while ((hw = glue(audio_pcm_hw_find_any_, TYPE)(s, hw))) { + if (audio_pcm_info_eq (&hw->info, as)) { + return hw; + } + } + return NULL; +} + +static HW *glue(audio_pcm_hw_add_new_, TYPE)(AudioState *s, + struct audsettings *as) +{ + HW *hw; + struct audio_driver *drv = s->drv; + + if (!glue (s->nb_hw_voices_, TYPE)) { + return NULL; + } + + if (audio_bug(__func__, !drv)) { + dolog ("No host audio driver\n"); + return NULL; + } + + if (audio_bug(__func__, !drv->pcm_ops)) { + dolog ("Host audio driver without pcm_ops\n"); + return NULL; + } + + hw = audio_calloc(__func__, 1, glue(drv->voice_size_, TYPE)); + if (!hw) { + dolog ("Can not allocate voice `%s' size %d\n", + drv->name, glue (drv->voice_size_, TYPE)); + return NULL; + } + + hw->s = s; + hw->pcm_ops = drv->pcm_ops; + + QLIST_INIT (&hw->sw_head); +#ifdef DAC + QLIST_INIT (&hw->cap_head); +#endif + if (glue (hw->pcm_ops->init_, TYPE) (hw, as, s->drv_opaque)) { + goto err0; + } + + if (audio_bug(__func__, hw->samples <= 0)) { + dolog("hw->samples=%zd\n", hw->samples); + goto err1; + } + + if (hw->info.is_float) { +#ifdef DAC + hw->clip = mixeng_clip_float[hw->info.nchannels == 2]; +#else + hw->conv = mixeng_conv_float[hw->info.nchannels == 2]; +#endif + } else { +#ifdef DAC + hw->clip = mixeng_clip +#else + hw->conv = mixeng_conv +#endif + [hw->info.nchannels == 2] + [hw->info.is_signed] + [hw->info.swap_endianness] + [audio_bits_to_index(hw->info.bits)]; + } + + glue(audio_pcm_hw_alloc_resources_, TYPE)(hw); + + QLIST_INSERT_HEAD (&s->glue (hw_head_, TYPE), hw, entries); + glue (s->nb_hw_voices_, TYPE) -= 1; +#ifdef DAC + audio_attach_capture (hw); +#endif + return hw; + + err1: + glue (hw->pcm_ops->fini_, TYPE) (hw); + err0: + g_free (hw); + return NULL; +} + +AudiodevPerDirectionOptions *glue(audio_get_pdo_, TYPE)(Audiodev *dev) +{ + switch (dev->driver) { + case AUDIODEV_DRIVER_NONE: + return dev->u.none.TYPE; + case AUDIODEV_DRIVER_ALSA: + return qapi_AudiodevAlsaPerDirectionOptions_base(dev->u.alsa.TYPE); + case AUDIODEV_DRIVER_COREAUDIO: + return qapi_AudiodevCoreaudioPerDirectionOptions_base( + dev->u.coreaudio.TYPE); + case AUDIODEV_DRIVER_DSOUND: + return dev->u.dsound.TYPE; + case AUDIODEV_DRIVER_JACK: + return qapi_AudiodevJackPerDirectionOptions_base(dev->u.jack.TYPE); + case AUDIODEV_DRIVER_OSS: + return qapi_AudiodevOssPerDirectionOptions_base(dev->u.oss.TYPE); + case AUDIODEV_DRIVER_PA: + return qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.TYPE); + case AUDIODEV_DRIVER_SDL: + return qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.TYPE); + case AUDIODEV_DRIVER_SPICE: + return dev->u.spice.TYPE; + case AUDIODEV_DRIVER_WAV: + return dev->u.wav.TYPE; + + case AUDIODEV_DRIVER__MAX: + break; + } + abort(); +} + +static HW *glue(audio_pcm_hw_add_, TYPE)(AudioState *s, struct audsettings *as) +{ + HW *hw; + AudiodevPerDirectionOptions *pdo = glue(audio_get_pdo_, TYPE)(s->dev); + + if (!pdo->mixing_engine || pdo->fixed_settings) { + hw = glue(audio_pcm_hw_add_new_, TYPE)(s, as); + if (!pdo->mixing_engine || hw) { + return hw; + } + } + + hw = glue(audio_pcm_hw_find_specific_, TYPE)(s, NULL, as); + if (hw) { + return hw; + } + + hw = glue(audio_pcm_hw_add_new_, TYPE)(s, as); + if (hw) { + return hw; + } + + return glue(audio_pcm_hw_find_any_, TYPE)(s, NULL); +} + +static SW *glue(audio_pcm_create_voice_pair_, TYPE)( + AudioState *s, + const char *sw_name, + struct audsettings *as + ) +{ + SW *sw; + HW *hw; + struct audsettings hw_as; + AudiodevPerDirectionOptions *pdo = glue(audio_get_pdo_, TYPE)(s->dev); + + if (pdo->fixed_settings) { + hw_as = audiodev_to_audsettings(pdo); + } else { + hw_as = *as; + } + + sw = audio_calloc(__func__, 1, sizeof(*sw)); + if (!sw) { + dolog ("Could not allocate soft voice `%s' (%zu bytes)\n", + sw_name ? sw_name : "unknown", sizeof (*sw)); + goto err1; + } + sw->s = s; + + hw = glue(audio_pcm_hw_add_, TYPE)(s, &hw_as); + if (!hw) { + goto err2; + } + + glue (audio_pcm_hw_add_sw_, TYPE) (hw, sw); + + if (glue (audio_pcm_sw_init_, TYPE) (sw, hw, sw_name, as)) { + goto err3; + } + + return sw; + +err3: + glue (audio_pcm_hw_del_sw_, TYPE) (sw); + glue (audio_pcm_hw_gc_, TYPE) (&hw); +err2: + g_free (sw); +err1: + return NULL; +} + +static void glue (audio_close_, TYPE) (SW *sw) +{ + glue (audio_pcm_sw_fini_, TYPE) (sw); + glue (audio_pcm_hw_del_sw_, TYPE) (sw); + glue (audio_pcm_hw_gc_, TYPE) (&sw->hw); + g_free (sw); +} + +void glue (AUD_close_, TYPE) (QEMUSoundCard *card, SW *sw) +{ + if (sw) { + if (audio_bug(__func__, !card)) { + dolog ("card=%p\n", card); + return; + } + + glue (audio_close_, TYPE) (sw); + } +} + +SW *glue (AUD_open_, TYPE) ( + QEMUSoundCard *card, + SW *sw, + const char *name, + void *callback_opaque , + audio_callback_fn callback_fn, + struct audsettings *as + ) +{ + AudioState *s; + AudiodevPerDirectionOptions *pdo; + + if (audio_bug(__func__, !card || !name || !callback_fn || !as)) { + dolog ("card=%p name=%p callback_fn=%p as=%p\n", + card, name, callback_fn, as); + goto fail; + } + + s = card->state; + pdo = glue(audio_get_pdo_, TYPE)(s->dev); + + ldebug ("open %s, freq %d, nchannels %d, fmt %d\n", + name, as->freq, as->nchannels, as->fmt); + + if (audio_bug(__func__, audio_validate_settings(as))) { + audio_print_settings (as); + goto fail; + } + + if (audio_bug(__func__, !s->drv)) { + dolog ("Can not open `%s' (no host audio driver)\n", name); + goto fail; + } + + if (sw && audio_pcm_info_eq (&sw->info, as)) { + return sw; + } + + if (!pdo->fixed_settings && sw) { + glue (AUD_close_, TYPE) (card, sw); + sw = NULL; + } + + if (sw) { + HW *hw = sw->hw; + + if (!hw) { + dolog ("Internal logic error voice `%s' has no hardware store\n", + SW_NAME (sw)); + goto fail; + } + + glue (audio_pcm_sw_fini_, TYPE) (sw); + if (glue (audio_pcm_sw_init_, TYPE) (sw, hw, name, as)) { + goto fail; + } + } else { + sw = glue(audio_pcm_create_voice_pair_, TYPE)(s, name, as); + if (!sw) { + dolog ("Failed to create voice `%s'\n", name); + return NULL; + } + } + + sw->card = card; + sw->vol = nominal_volume; + sw->callback.fn = callback_fn; + sw->callback.opaque = callback_opaque; + +#ifdef DEBUG_AUDIO + dolog ("%s\n", name); + audio_pcm_print_info ("hw", &sw->hw->info); + audio_pcm_print_info ("sw", &sw->info); +#endif + + return sw; + + fail: + glue (AUD_close_, TYPE) (card, sw); + return NULL; +} + +int glue (AUD_is_active_, TYPE) (SW *sw) +{ + return sw ? sw->active : 0; +} + +void glue (AUD_init_time_stamp_, TYPE) (SW *sw, QEMUAudioTimeStamp *ts) +{ + if (!sw) { + return; + } + + ts->old_ts = sw->hw->ts_helper; +} + +uint64_t glue (AUD_get_elapsed_usec_, TYPE) (SW *sw, QEMUAudioTimeStamp *ts) +{ + uint64_t delta, cur_ts, old_ts; + + if (!sw) { + return 0; + } + + cur_ts = sw->hw->ts_helper; + old_ts = ts->old_ts; + /* dolog ("cur %" PRId64 " old %" PRId64 "\n", cur_ts, old_ts); */ + + if (cur_ts >= old_ts) { + delta = cur_ts - old_ts; + } else { + delta = UINT64_MAX - old_ts + cur_ts; + } + + if (!delta) { + return 0; + } + + return muldiv64 (delta, sw->hw->info.freq, 1000000); +} + +#undef TYPE +#undef HW +#undef SW +#undef HWBUF +#undef NAME diff --git a/audio/audio_win_int.c b/audio/audio_win_int.c new file mode 100644 index 000000000..5ea8157df --- /dev/null +++ b/audio/audio_win_int.c @@ -0,0 +1,131 @@ +/* public domain */ + +#include "qemu/osdep.h" +#include "qemu-common.h" + +#define AUDIO_CAP "win-int" +#include <windows.h> +#include <mmreg.h> +#include <mmsystem.h> + +#include "audio.h" +#include "audio_int.h" +#include "audio_win_int.h" + +int waveformat_from_audio_settings (WAVEFORMATEX *wfx, + struct audsettings *as) +{ + memset (wfx, 0, sizeof (*wfx)); + + wfx->nChannels = as->nchannels; + wfx->nSamplesPerSec = as->freq; + wfx->nAvgBytesPerSec = as->freq << (as->nchannels == 2); + wfx->nBlockAlign = 1 << (as->nchannels == 2); + wfx->cbSize = 0; + + switch (as->fmt) { + case AUDIO_FORMAT_S8: + case AUDIO_FORMAT_U8: + wfx->wFormatTag = WAVE_FORMAT_PCM; + wfx->wBitsPerSample = 8; + break; + + case AUDIO_FORMAT_S16: + case AUDIO_FORMAT_U16: + wfx->wFormatTag = WAVE_FORMAT_PCM; + wfx->wBitsPerSample = 16; + wfx->nAvgBytesPerSec <<= 1; + wfx->nBlockAlign <<= 1; + break; + + case AUDIO_FORMAT_S32: + case AUDIO_FORMAT_U32: + wfx->wFormatTag = WAVE_FORMAT_PCM; + wfx->wBitsPerSample = 32; + wfx->nAvgBytesPerSec <<= 2; + wfx->nBlockAlign <<= 2; + break; + + case AUDIO_FORMAT_F32: + wfx->wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + wfx->wBitsPerSample = 32; + wfx->nAvgBytesPerSec <<= 2; + wfx->nBlockAlign <<= 2; + break; + + default: + dolog("Internal logic error: Bad audio format %d\n", as->fmt); + return -1; + } + + return 0; +} + +int waveformat_to_audio_settings (WAVEFORMATEX *wfx, + struct audsettings *as) +{ + if (!wfx->nSamplesPerSec) { + dolog ("Invalid wave format, frequency is zero\n"); + return -1; + } + as->freq = wfx->nSamplesPerSec; + + switch (wfx->nChannels) { + case 1: + as->nchannels = 1; + break; + + case 2: + as->nchannels = 2; + break; + + default: + dolog ( + "Invalid wave format, number of channels is not 1 or 2, but %d\n", + wfx->nChannels + ); + return -1; + } + + if (wfx->wFormatTag == WAVE_FORMAT_PCM) { + switch (wfx->wBitsPerSample) { + case 8: + as->fmt = AUDIO_FORMAT_U8; + break; + + case 16: + as->fmt = AUDIO_FORMAT_S16; + break; + + case 32: + as->fmt = AUDIO_FORMAT_S32; + break; + + default: + dolog("Invalid PCM wave format, bits per sample is not " + "8, 16 or 32, but %d\n", + wfx->wBitsPerSample); + return -1; + } + } else if (wfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { + switch (wfx->wBitsPerSample) { + case 32: + as->fmt = AUDIO_FORMAT_F32; + break; + + default: + dolog("Invalid IEEE_FLOAT wave format, bits per sample is not " + "32, but %d\n", + wfx->wBitsPerSample); + return -1; + } + } else { + dolog("Invalid wave format, tag is not PCM and not IEEE_FLOAT, " + "but %d\n", + wfx->wFormatTag); + return -1; + } + + return 0; +} + diff --git a/audio/audio_win_int.h b/audio/audio_win_int.h new file mode 100644 index 000000000..fa5b3cb80 --- /dev/null +++ b/audio/audio_win_int.h @@ -0,0 +1,10 @@ +#ifndef AUDIO_WIN_INT_H +#define AUDIO_WIN_INT_H + +int waveformat_from_audio_settings (WAVEFORMATEX *wfx, + struct audsettings *as); + +int waveformat_to_audio_settings (WAVEFORMATEX *wfx, + struct audsettings *as); + +#endif /* AUDIO_WIN_INT_H */ diff --git a/audio/coreaudio.c b/audio/coreaudio.c new file mode 100644 index 000000000..d8a21d3e5 --- /dev/null +++ b/audio/coreaudio.c @@ -0,0 +1,681 @@ +/* + * QEMU OS X CoreAudio audio driver + * + * Copyright (c) 2005 Mike Kronenberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include <CoreAudio/CoreAudio.h> +#include <pthread.h> /* pthread_X */ + +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "audio.h" + +#define AUDIO_CAP "coreaudio" +#include "audio_int.h" + +typedef struct coreaudioVoiceOut { + HWVoiceOut hw; + pthread_mutex_t buf_mutex; + AudioDeviceID outputDeviceID; + int frameSizeSetting; + uint32_t bufferCount; + UInt32 audioDevicePropertyBufferFrameSize; + AudioDeviceIOProcID ioprocid; + bool enabled; +} coreaudioVoiceOut; + +static const AudioObjectPropertyAddress voice_addr = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster +}; + +static OSStatus coreaudio_get_voice(AudioDeviceID *id) +{ + UInt32 size = sizeof(*id); + + return AudioObjectGetPropertyData(kAudioObjectSystemObject, + &voice_addr, + 0, + NULL, + &size, + id); +} + +static OSStatus coreaudio_get_framesizerange(AudioDeviceID id, + AudioValueRange *framerange) +{ + UInt32 size = sizeof(*framerange); + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyBufferFrameSizeRange, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + return AudioObjectGetPropertyData(id, + &addr, + 0, + NULL, + &size, + framerange); +} + +static OSStatus coreaudio_get_framesize(AudioDeviceID id, UInt32 *framesize) +{ + UInt32 size = sizeof(*framesize); + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyBufferFrameSize, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + return AudioObjectGetPropertyData(id, + &addr, + 0, + NULL, + &size, + framesize); +} + +static OSStatus coreaudio_set_framesize(AudioDeviceID id, UInt32 *framesize) +{ + UInt32 size = sizeof(*framesize); + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyBufferFrameSize, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + return AudioObjectSetPropertyData(id, + &addr, + 0, + NULL, + size, + framesize); +} + +static OSStatus coreaudio_set_streamformat(AudioDeviceID id, + AudioStreamBasicDescription *d) +{ + UInt32 size = sizeof(*d); + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyStreamFormat, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + return AudioObjectSetPropertyData(id, + &addr, + 0, + NULL, + size, + d); +} + +static OSStatus coreaudio_get_isrunning(AudioDeviceID id, UInt32 *result) +{ + UInt32 size = sizeof(*result); + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyDeviceIsRunning, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + return AudioObjectGetPropertyData(id, + &addr, + 0, + NULL, + &size, + result); +} + +static void coreaudio_logstatus (OSStatus status) +{ + const char *str = "BUG"; + + switch (status) { + case kAudioHardwareNoError: + str = "kAudioHardwareNoError"; + break; + + case kAudioHardwareNotRunningError: + str = "kAudioHardwareNotRunningError"; + break; + + case kAudioHardwareUnspecifiedError: + str = "kAudioHardwareUnspecifiedError"; + break; + + case kAudioHardwareUnknownPropertyError: + str = "kAudioHardwareUnknownPropertyError"; + break; + + case kAudioHardwareBadPropertySizeError: + str = "kAudioHardwareBadPropertySizeError"; + break; + + case kAudioHardwareIllegalOperationError: + str = "kAudioHardwareIllegalOperationError"; + break; + + case kAudioHardwareBadDeviceError: + str = "kAudioHardwareBadDeviceError"; + break; + + case kAudioHardwareBadStreamError: + str = "kAudioHardwareBadStreamError"; + break; + + case kAudioHardwareUnsupportedOperationError: + str = "kAudioHardwareUnsupportedOperationError"; + break; + + case kAudioDeviceUnsupportedFormatError: + str = "kAudioDeviceUnsupportedFormatError"; + break; + + case kAudioDevicePermissionsError: + str = "kAudioDevicePermissionsError"; + break; + + default: + AUD_log (AUDIO_CAP, "Reason: status code %" PRId32 "\n", (int32_t)status); + return; + } + + AUD_log (AUDIO_CAP, "Reason: %s\n", str); +} + +static void GCC_FMT_ATTR (2, 3) coreaudio_logerr ( + OSStatus status, + const char *fmt, + ... + ) +{ + va_list ap; + + va_start (ap, fmt); + AUD_log (AUDIO_CAP, fmt, ap); + va_end (ap); + + coreaudio_logstatus (status); +} + +static void GCC_FMT_ATTR (3, 4) coreaudio_logerr2 ( + OSStatus status, + const char *typ, + const char *fmt, + ... + ) +{ + va_list ap; + + AUD_log (AUDIO_CAP, "Could not initialize %s\n", typ); + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + coreaudio_logstatus (status); +} + +#define coreaudio_playback_logerr(status, ...) \ + coreaudio_logerr2(status, "playback", __VA_ARGS__) + +static int coreaudio_buf_lock (coreaudioVoiceOut *core, const char *fn_name) +{ + int err; + + err = pthread_mutex_lock (&core->buf_mutex); + if (err) { + dolog ("Could not lock voice for %s\nReason: %s\n", + fn_name, strerror (err)); + return -1; + } + return 0; +} + +static int coreaudio_buf_unlock (coreaudioVoiceOut *core, const char *fn_name) +{ + int err; + + err = pthread_mutex_unlock (&core->buf_mutex); + if (err) { + dolog ("Could not unlock voice for %s\nReason: %s\n", + fn_name, strerror (err)); + return -1; + } + return 0; +} + +#define COREAUDIO_WRAPPER_FUNC(name, ret_type, args_decl, args) \ + static ret_type glue(coreaudio_, name)args_decl \ + { \ + coreaudioVoiceOut *core = (coreaudioVoiceOut *) hw; \ + ret_type ret; \ + \ + if (coreaudio_buf_lock(core, "coreaudio_" #name)) { \ + return 0; \ + } \ + \ + ret = glue(audio_generic_, name)args; \ + \ + coreaudio_buf_unlock(core, "coreaudio_" #name); \ + return ret; \ + } +COREAUDIO_WRAPPER_FUNC(get_buffer_out, void *, (HWVoiceOut *hw, size_t *size), + (hw, size)) +COREAUDIO_WRAPPER_FUNC(put_buffer_out, size_t, + (HWVoiceOut *hw, void *buf, size_t size), + (hw, buf, size)) +COREAUDIO_WRAPPER_FUNC(write, size_t, (HWVoiceOut *hw, void *buf, size_t size), + (hw, buf, size)) +#undef COREAUDIO_WRAPPER_FUNC + +/* + * callback to feed audiooutput buffer. called without iothread lock. + * allowed to lock "buf_mutex", but disallowed to have any other locks. + */ +static OSStatus audioDeviceIOProc( + AudioDeviceID inDevice, + const AudioTimeStamp *inNow, + const AudioBufferList *inInputData, + const AudioTimeStamp *inInputTime, + AudioBufferList *outOutputData, + const AudioTimeStamp *inOutputTime, + void *hwptr) +{ + UInt32 frameCount, pending_frames; + void *out = outOutputData->mBuffers[0].mData; + HWVoiceOut *hw = hwptr; + coreaudioVoiceOut *core = (coreaudioVoiceOut *) hwptr; + size_t len; + + if (coreaudio_buf_lock (core, "audioDeviceIOProc")) { + inInputTime = 0; + return 0; + } + + if (inDevice != core->outputDeviceID) { + coreaudio_buf_unlock (core, "audioDeviceIOProc(old device)"); + return 0; + } + + frameCount = core->audioDevicePropertyBufferFrameSize; + pending_frames = hw->pending_emul / hw->info.bytes_per_frame; + + /* if there are not enough samples, set signal and return */ + if (pending_frames < frameCount) { + inInputTime = 0; + coreaudio_buf_unlock (core, "audioDeviceIOProc(empty)"); + return 0; + } + + len = frameCount * hw->info.bytes_per_frame; + while (len) { + size_t write_len; + ssize_t start = ((ssize_t) hw->pos_emul) - hw->pending_emul; + if (start < 0) { + start += hw->size_emul; + } + assert(start >= 0 && start < hw->size_emul); + + write_len = MIN(MIN(hw->pending_emul, len), + hw->size_emul - start); + + memcpy(out, hw->buf_emul + start, write_len); + hw->pending_emul -= write_len; + len -= write_len; + out += write_len; + } + + coreaudio_buf_unlock (core, "audioDeviceIOProc"); + return 0; +} + +static OSStatus init_out_device(coreaudioVoiceOut *core) +{ + OSStatus status; + AudioValueRange frameRange; + + AudioStreamBasicDescription streamBasicDescription = { + .mBitsPerChannel = core->hw.info.bits, + .mBytesPerFrame = core->hw.info.bytes_per_frame, + .mBytesPerPacket = core->hw.info.bytes_per_frame, + .mChannelsPerFrame = core->hw.info.nchannels, + .mFormatFlags = kLinearPCMFormatFlagIsFloat, + .mFormatID = kAudioFormatLinearPCM, + .mFramesPerPacket = 1, + .mSampleRate = core->hw.info.freq + }; + + status = coreaudio_get_voice(&core->outputDeviceID); + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not get default output Device\n"); + return status; + } + if (core->outputDeviceID == kAudioDeviceUnknown) { + dolog ("Could not initialize playback - Unknown Audiodevice\n"); + return status; + } + + /* get minimum and maximum buffer frame sizes */ + status = coreaudio_get_framesizerange(core->outputDeviceID, + &frameRange); + if (status == kAudioHardwareBadObjectError) { + return 0; + } + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not get device buffer frame range\n"); + return status; + } + + if (frameRange.mMinimum > core->frameSizeSetting) { + core->audioDevicePropertyBufferFrameSize = (UInt32) frameRange.mMinimum; + dolog ("warning: Upsizing Buffer Frames to %f\n", frameRange.mMinimum); + } else if (frameRange.mMaximum < core->frameSizeSetting) { + core->audioDevicePropertyBufferFrameSize = (UInt32) frameRange.mMaximum; + dolog ("warning: Downsizing Buffer Frames to %f\n", frameRange.mMaximum); + } else { + core->audioDevicePropertyBufferFrameSize = core->frameSizeSetting; + } + + /* set Buffer Frame Size */ + status = coreaudio_set_framesize(core->outputDeviceID, + &core->audioDevicePropertyBufferFrameSize); + if (status == kAudioHardwareBadObjectError) { + return 0; + } + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not set device buffer frame size %" PRIu32 "\n", + (uint32_t)core->audioDevicePropertyBufferFrameSize); + return status; + } + + /* get Buffer Frame Size */ + status = coreaudio_get_framesize(core->outputDeviceID, + &core->audioDevicePropertyBufferFrameSize); + if (status == kAudioHardwareBadObjectError) { + return 0; + } + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not get device buffer frame size\n"); + return status; + } + core->hw.samples = core->bufferCount * core->audioDevicePropertyBufferFrameSize; + + /* set Samplerate */ + status = coreaudio_set_streamformat(core->outputDeviceID, + &streamBasicDescription); + if (status == kAudioHardwareBadObjectError) { + return 0; + } + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not set samplerate %lf\n", + streamBasicDescription.mSampleRate); + core->outputDeviceID = kAudioDeviceUnknown; + return status; + } + + /* + * set Callback. + * + * On macOS 11.3.1, Core Audio calls AudioDeviceIOProc after calling an + * internal function named HALB_Mutex::Lock(), which locks a mutex in + * HALB_IOThread::Entry(void*). HALB_Mutex::Lock() is also called in + * AudioObjectGetPropertyData, which is called by coreaudio driver. + * Therefore, the specified callback must be designed to avoid a deadlock + * with the callers of AudioObjectGetPropertyData. + */ + core->ioprocid = NULL; + status = AudioDeviceCreateIOProcID(core->outputDeviceID, + audioDeviceIOProc, + &core->hw, + &core->ioprocid); + if (status == kAudioHardwareBadDeviceError) { + return 0; + } + if (status != kAudioHardwareNoError || core->ioprocid == NULL) { + coreaudio_playback_logerr (status, "Could not set IOProc\n"); + core->outputDeviceID = kAudioDeviceUnknown; + return status; + } + + return 0; +} + +static void fini_out_device(coreaudioVoiceOut *core) +{ + OSStatus status; + UInt32 isrunning; + + /* stop playback */ + status = coreaudio_get_isrunning(core->outputDeviceID, &isrunning); + if (status != kAudioHardwareBadObjectError) { + if (status != kAudioHardwareNoError) { + coreaudio_logerr(status, + "Could not determine whether Device is playing\n"); + } + + if (isrunning) { + status = AudioDeviceStop(core->outputDeviceID, core->ioprocid); + if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { + coreaudio_logerr(status, "Could not stop playback\n"); + } + } + } + + /* remove callback */ + status = AudioDeviceDestroyIOProcID(core->outputDeviceID, + core->ioprocid); + if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { + coreaudio_logerr(status, "Could not remove IOProc\n"); + } + core->outputDeviceID = kAudioDeviceUnknown; +} + +static void update_device_playback_state(coreaudioVoiceOut *core) +{ + OSStatus status; + UInt32 isrunning; + + status = coreaudio_get_isrunning(core->outputDeviceID, &isrunning); + if (status != kAudioHardwareNoError) { + if (status != kAudioHardwareBadObjectError) { + coreaudio_logerr(status, + "Could not determine whether Device is playing\n"); + } + + return; + } + + if (core->enabled) { + /* start playback */ + if (!isrunning) { + status = AudioDeviceStart(core->outputDeviceID, core->ioprocid); + if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { + coreaudio_logerr (status, "Could not resume playback\n"); + } + } + } else { + /* stop playback */ + if (isrunning) { + status = AudioDeviceStop(core->outputDeviceID, + core->ioprocid); + if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { + coreaudio_logerr(status, "Could not pause playback\n"); + } + } + } +} + +/* called without iothread lock. */ +static OSStatus handle_voice_change( + AudioObjectID in_object_id, + UInt32 in_number_addresses, + const AudioObjectPropertyAddress *in_addresses, + void *in_client_data) +{ + OSStatus status; + coreaudioVoiceOut *core = in_client_data; + + qemu_mutex_lock_iothread(); + + if (core->outputDeviceID) { + fini_out_device(core); + } + + status = init_out_device(core); + if (!status) { + update_device_playback_state(core); + } + + qemu_mutex_unlock_iothread(); + return status; +} + +static int coreaudio_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + OSStatus status; + coreaudioVoiceOut *core = (coreaudioVoiceOut *) hw; + int err; + Audiodev *dev = drv_opaque; + AudiodevCoreaudioPerDirectionOptions *cpdo = dev->u.coreaudio.out; + struct audsettings obt_as; + + /* create mutex */ + err = pthread_mutex_init(&core->buf_mutex, NULL); + if (err) { + dolog("Could not create mutex\nReason: %s\n", strerror (err)); + return -1; + } + + obt_as = *as; + as = &obt_as; + as->fmt = AUDIO_FORMAT_F32; + audio_pcm_init_info (&hw->info, as); + + core->frameSizeSetting = audio_buffer_frames( + qapi_AudiodevCoreaudioPerDirectionOptions_base(cpdo), as, 11610); + + core->bufferCount = cpdo->has_buffer_count ? cpdo->buffer_count : 4; + + status = AudioObjectAddPropertyListener(kAudioObjectSystemObject, + &voice_addr, handle_voice_change, + core); + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr (status, + "Could not listen to voice property change\n"); + return -1; + } + + if (init_out_device(core)) { + status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, + &voice_addr, + handle_voice_change, + core); + if (status != kAudioHardwareNoError) { + coreaudio_playback_logerr(status, + "Could not remove voice property change listener\n"); + } + } + + return 0; +} + +static void coreaudio_fini_out (HWVoiceOut *hw) +{ + OSStatus status; + int err; + coreaudioVoiceOut *core = (coreaudioVoiceOut *) hw; + + status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, + &voice_addr, + handle_voice_change, + core); + if (status != kAudioHardwareNoError) { + coreaudio_logerr(status, "Could not remove voice property change listener\n"); + } + + fini_out_device(core); + + /* destroy mutex */ + err = pthread_mutex_destroy(&core->buf_mutex); + if (err) { + dolog("Could not destroy mutex\nReason: %s\n", strerror (err)); + } +} + +static void coreaudio_enable_out(HWVoiceOut *hw, bool enable) +{ + coreaudioVoiceOut *core = (coreaudioVoiceOut *) hw; + + core->enabled = enable; + update_device_playback_state(core); +} + +static void *coreaudio_audio_init(Audiodev *dev) +{ + return dev; +} + +static void coreaudio_audio_fini (void *opaque) +{ +} + +static struct audio_pcm_ops coreaudio_pcm_ops = { + .init_out = coreaudio_init_out, + .fini_out = coreaudio_fini_out, + /* wrapper for audio_generic_write */ + .write = coreaudio_write, + /* wrapper for audio_generic_get_buffer_out */ + .get_buffer_out = coreaudio_get_buffer_out, + /* wrapper for audio_generic_put_buffer_out */ + .put_buffer_out = coreaudio_put_buffer_out, + .enable_out = coreaudio_enable_out +}; + +static struct audio_driver coreaudio_audio_driver = { + .name = "coreaudio", + .descr = "CoreAudio http://developer.apple.com/audio/coreaudio.html", + .init = coreaudio_audio_init, + .fini = coreaudio_audio_fini, + .pcm_ops = &coreaudio_pcm_ops, + .can_be_default = 1, + .max_voices_out = 1, + .max_voices_in = 0, + .voice_size_out = sizeof (coreaudioVoiceOut), + .voice_size_in = 0 +}; + +static void register_audio_coreaudio(void) +{ + audio_driver_register(&coreaudio_audio_driver); +} +type_init(register_audio_coreaudio); diff --git a/audio/dsound_template.h b/audio/dsound_template.h new file mode 100644 index 000000000..0678f2de3 --- /dev/null +++ b/audio/dsound_template.h @@ -0,0 +1,280 @@ +/* + * QEMU DirectSound audio driver header + * + * Copyright (c) 2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifdef DSBTYPE_IN +#define NAME "capture buffer" +#define NAME2 "DirectSoundCapture" +#define TYPE in +#define IFACE IDirectSoundCaptureBuffer +#define BUFPTR LPDIRECTSOUNDCAPTUREBUFFER +#define FIELD dsound_capture_buffer +#define FIELD2 dsound_capture +#define HWVOICE HWVoiceIn +#define DSOUNDVOICE DSoundVoiceIn +#else +#define NAME "playback buffer" +#define NAME2 "DirectSound" +#define TYPE out +#define IFACE IDirectSoundBuffer +#define BUFPTR LPDIRECTSOUNDBUFFER +#define FIELD dsound_buffer +#define FIELD2 dsound +#define HWVOICE HWVoiceOut +#define DSOUNDVOICE DSoundVoiceOut +#endif + +static int glue (dsound_unlock_, TYPE) ( + BUFPTR buf, + LPVOID p1, + LPVOID p2, + DWORD blen1, + DWORD blen2 + ) +{ + HRESULT hr; + + hr = glue (IFACE, _Unlock) (buf, p1, blen1, p2, blen2); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not unlock " NAME "\n"); + return -1; + } + + return 0; +} + +static int glue (dsound_lock_, TYPE) ( + BUFPTR buf, + struct audio_pcm_info *info, + DWORD pos, + DWORD len, + LPVOID *p1p, + LPVOID *p2p, + DWORD *blen1p, + DWORD *blen2p, + int entire, + dsound *s + ) +{ + HRESULT hr; + DWORD flag; + +#ifdef DSBTYPE_IN + flag = entire ? DSCBLOCK_ENTIREBUFFER : 0; +#else + flag = entire ? DSBLOCK_ENTIREBUFFER : 0; +#endif + hr = glue(IFACE, _Lock)(buf, pos, len, p1p, blen1p, p2p, blen2p, flag); + + if (FAILED (hr)) { +#ifndef DSBTYPE_IN + if (hr == DSERR_BUFFERLOST) { + if (glue (dsound_restore_, TYPE) (buf, s)) { + dsound_logerr (hr, "Could not lock " NAME "\n"); + } + goto fail; + } +#endif + dsound_logerr (hr, "Could not lock " NAME "\n"); + goto fail; + } + + if ((p1p && *p1p && (*blen1p % info->bytes_per_frame)) || + (p2p && *p2p && (*blen2p % info->bytes_per_frame))) { + dolog("DirectSound returned misaligned buffer %ld %ld\n", + *blen1p, *blen2p); + glue(dsound_unlock_, TYPE)(buf, *p1p, p2p ? *p2p : NULL, *blen1p, + blen2p ? *blen2p : 0); + goto fail; + } + + if (p1p && !*p1p && *blen1p) { + dolog("warning: !p1 && blen1=%ld\n", *blen1p); + *blen1p = 0; + } + + if (p2p && !*p2p && *blen2p) { + dolog("warning: !p2 && blen2=%ld\n", *blen2p); + *blen2p = 0; + } + + return 0; + + fail: + *p1p = NULL - 1; + *blen1p = -1; + if (p2p) { + *p2p = NULL - 1; + *blen2p = -1; + } + return -1; +} + +#ifdef DSBTYPE_IN +static void dsound_fini_in (HWVoiceIn *hw) +#else +static void dsound_fini_out (HWVoiceOut *hw) +#endif +{ + HRESULT hr; +#ifdef DSBTYPE_IN + DSoundVoiceIn *ds = (DSoundVoiceIn *) hw; +#else + DSoundVoiceOut *ds = (DSoundVoiceOut *) hw; +#endif + + if (ds->FIELD) { + hr = glue (IFACE, _Stop) (ds->FIELD); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not stop " NAME "\n"); + } + + hr = glue (IFACE, _Release) (ds->FIELD); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not release " NAME "\n"); + } + ds->FIELD = NULL; + } +} + +#ifdef DSBTYPE_IN +static int dsound_init_in(HWVoiceIn *hw, struct audsettings *as, + void *drv_opaque) +#else +static int dsound_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +#endif +{ + int err; + HRESULT hr; + dsound *s = drv_opaque; + WAVEFORMATEX wfx; + struct audsettings obt_as; +#ifdef DSBTYPE_IN + const char *typ = "ADC"; + DSoundVoiceIn *ds = (DSoundVoiceIn *) hw; + DSCBUFFERDESC bd; + DSCBCAPS bc; + AudiodevPerDirectionOptions *pdo = s->dev->u.dsound.in; +#else + const char *typ = "DAC"; + DSoundVoiceOut *ds = (DSoundVoiceOut *) hw; + DSBUFFERDESC bd; + DSBCAPS bc; + AudiodevPerDirectionOptions *pdo = s->dev->u.dsound.out; +#endif + + if (!s->FIELD2) { + dolog ("Attempt to initialize voice without " NAME2 " object\n"); + return -1; + } + + err = waveformat_from_audio_settings (&wfx, as); + if (err) { + return -1; + } + + memset (&bd, 0, sizeof (bd)); + bd.dwSize = sizeof (bd); + bd.lpwfxFormat = &wfx; + bd.dwBufferBytes = audio_buffer_bytes(pdo, as, 92880); +#ifdef DSBTYPE_IN + hr = IDirectSoundCapture_CreateCaptureBuffer ( + s->dsound_capture, + &bd, + &ds->dsound_capture_buffer, + NULL + ); +#else + bd.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2; + hr = IDirectSound_CreateSoundBuffer ( + s->dsound, + &bd, + &ds->dsound_buffer, + NULL + ); +#endif + + if (FAILED (hr)) { + dsound_logerr2 (hr, typ, "Could not create " NAME "\n"); + return -1; + } + + hr = glue (IFACE, _GetFormat) (ds->FIELD, &wfx, sizeof (wfx), NULL); + if (FAILED (hr)) { + dsound_logerr2 (hr, typ, "Could not get " NAME " format\n"); + goto fail0; + } + +#ifdef DEBUG_DSOUND + dolog (NAME "\n"); + print_wave_format (&wfx); +#endif + + memset (&bc, 0, sizeof (bc)); + bc.dwSize = sizeof (bc); + + hr = glue (IFACE, _GetCaps) (ds->FIELD, &bc); + if (FAILED (hr)) { + dsound_logerr2 (hr, typ, "Could not get " NAME " format\n"); + goto fail0; + } + + err = waveformat_to_audio_settings (&wfx, &obt_as); + if (err) { + goto fail0; + } + + ds->first_time = true; + obt_as.endianness = 0; + audio_pcm_init_info (&hw->info, &obt_as); + + if (bc.dwBufferBytes % hw->info.bytes_per_frame) { + dolog ( + "GetCaps returned misaligned buffer size %ld, alignment %d\n", + bc.dwBufferBytes, hw->info.bytes_per_frame + ); + } + hw->size_emul = bc.dwBufferBytes; + hw->samples = bc.dwBufferBytes / hw->info.bytes_per_frame; + ds->s = s; + +#ifdef DEBUG_DSOUND + dolog ("caps %ld, desc %ld\n", + bc.dwBufferBytes, bd.dwBufferBytes); +#endif + return 0; + + fail0: + glue (dsound_fini_, TYPE) (hw); + return -1; +} + +#undef NAME +#undef NAME2 +#undef TYPE +#undef IFACE +#undef BUFPTR +#undef FIELD +#undef FIELD2 +#undef HWVOICE +#undef DSOUNDVOICE diff --git a/audio/dsoundaudio.c b/audio/dsoundaudio.c new file mode 100644 index 000000000..cfc79c129 --- /dev/null +++ b/audio/dsoundaudio.c @@ -0,0 +1,732 @@ +/* + * QEMU DirectSound audio driver + * + * Copyright (c) 2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * SEAL 1.07 by Carlos 'pel' Hasan was used as documentation + */ + +#include "qemu/osdep.h" +#include "audio.h" + +#define AUDIO_CAP "dsound" +#include "audio_int.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" + +#include <windows.h> +#include <mmsystem.h> +#include <objbase.h> +#include <dsound.h> + +#include "audio_win_int.h" + +/* #define DEBUG_DSOUND */ + +typedef struct { + LPDIRECTSOUND dsound; + LPDIRECTSOUNDCAPTURE dsound_capture; + struct audsettings settings; + Audiodev *dev; +} dsound; + +typedef struct { + HWVoiceOut hw; + LPDIRECTSOUNDBUFFER dsound_buffer; + bool first_time; + dsound *s; +} DSoundVoiceOut; + +typedef struct { + HWVoiceIn hw; + LPDIRECTSOUNDCAPTUREBUFFER dsound_capture_buffer; + bool first_time; + dsound *s; +} DSoundVoiceIn; + +static void dsound_log_hresult (HRESULT hr) +{ + const char *str = "BUG"; + + switch (hr) { + case DS_OK: + str = "The method succeeded"; + break; +#ifdef DS_NO_VIRTUALIZATION + case DS_NO_VIRTUALIZATION: + str = "The buffer was created, but another 3D algorithm was substituted"; + break; +#endif +#ifdef DS_INCOMPLETE + case DS_INCOMPLETE: + str = "The method succeeded, but not all the optional effects were obtained"; + break; +#endif +#ifdef DSERR_ACCESSDENIED + case DSERR_ACCESSDENIED: + str = "The request failed because access was denied"; + break; +#endif +#ifdef DSERR_ALLOCATED + case DSERR_ALLOCATED: + str = "The request failed because resources, " + "such as a priority level, were already in use " + "by another caller"; + break; +#endif +#ifdef DSERR_ALREADYINITIALIZED + case DSERR_ALREADYINITIALIZED: + str = "The object is already initialized"; + break; +#endif +#ifdef DSERR_BADFORMAT + case DSERR_BADFORMAT: + str = "The specified wave format is not supported"; + break; +#endif +#ifdef DSERR_BADSENDBUFFERGUID + case DSERR_BADSENDBUFFERGUID: + str = "The GUID specified in an audiopath file " + "does not match a valid mix-in buffer"; + break; +#endif +#ifdef DSERR_BUFFERLOST + case DSERR_BUFFERLOST: + str = "The buffer memory has been lost and must be restored"; + break; +#endif +#ifdef DSERR_BUFFERTOOSMALL + case DSERR_BUFFERTOOSMALL: + str = "The buffer size is not great enough to " + "enable effects processing"; + break; +#endif +#ifdef DSERR_CONTROLUNAVAIL + case DSERR_CONTROLUNAVAIL: + str = "The buffer control (volume, pan, and so on) " + "requested by the caller is not available. " + "Controls must be specified when the buffer is created, " + "using the dwFlags member of DSBUFFERDESC"; + break; +#endif +#ifdef DSERR_DS8_REQUIRED + case DSERR_DS8_REQUIRED: + str = "A DirectSound object of class CLSID_DirectSound8 or later " + "is required for the requested functionality. " + "For more information, see IDirectSound8 Interface"; + break; +#endif +#ifdef DSERR_FXUNAVAILABLE + case DSERR_FXUNAVAILABLE: + str = "The effects requested could not be found on the system, " + "or they are in the wrong order or in the wrong location; " + "for example, an effect expected in hardware " + "was found in software"; + break; +#endif +#ifdef DSERR_GENERIC + case DSERR_GENERIC: + str = "An undetermined error occurred inside the DirectSound subsystem"; + break; +#endif +#ifdef DSERR_INVALIDCALL + case DSERR_INVALIDCALL: + str = "This function is not valid for the current state of this object"; + break; +#endif +#ifdef DSERR_INVALIDPARAM + case DSERR_INVALIDPARAM: + str = "An invalid parameter was passed to the returning function"; + break; +#endif +#ifdef DSERR_NOAGGREGATION + case DSERR_NOAGGREGATION: + str = "The object does not support aggregation"; + break; +#endif +#ifdef DSERR_NODRIVER + case DSERR_NODRIVER: + str = "No sound driver is available for use, " + "or the given GUID is not a valid DirectSound device ID"; + break; +#endif +#ifdef DSERR_NOINTERFACE + case DSERR_NOINTERFACE: + str = "The requested COM interface is not available"; + break; +#endif +#ifdef DSERR_OBJECTNOTFOUND + case DSERR_OBJECTNOTFOUND: + str = "The requested object was not found"; + break; +#endif +#ifdef DSERR_OTHERAPPHASPRIO + case DSERR_OTHERAPPHASPRIO: + str = "Another application has a higher priority level, " + "preventing this call from succeeding"; + break; +#endif +#ifdef DSERR_OUTOFMEMORY + case DSERR_OUTOFMEMORY: + str = "The DirectSound subsystem could not allocate " + "sufficient memory to complete the caller's request"; + break; +#endif +#ifdef DSERR_PRIOLEVELNEEDED + case DSERR_PRIOLEVELNEEDED: + str = "A cooperative level of DSSCL_PRIORITY or higher is required"; + break; +#endif +#ifdef DSERR_SENDLOOP + case DSERR_SENDLOOP: + str = "A circular loop of send effects was detected"; + break; +#endif +#ifdef DSERR_UNINITIALIZED + case DSERR_UNINITIALIZED: + str = "The Initialize method has not been called " + "or has not been called successfully " + "before other methods were called"; + break; +#endif +#ifdef DSERR_UNSUPPORTED + case DSERR_UNSUPPORTED: + str = "The function called is not supported at this time"; + break; +#endif + default: + AUD_log (AUDIO_CAP, "Reason: Unknown (HRESULT 0x%lx)\n", hr); + return; + } + + AUD_log (AUDIO_CAP, "Reason: %s\n", str); +} + +static void GCC_FMT_ATTR (2, 3) dsound_logerr ( + HRESULT hr, + const char *fmt, + ... + ) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + dsound_log_hresult (hr); +} + +static void GCC_FMT_ATTR (3, 4) dsound_logerr2 ( + HRESULT hr, + const char *typ, + const char *fmt, + ... + ) +{ + va_list ap; + + AUD_log (AUDIO_CAP, "Could not initialize %s\n", typ); + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + dsound_log_hresult (hr); +} + +#ifdef DEBUG_DSOUND +static void print_wave_format (WAVEFORMATEX *wfx) +{ + dolog ("tag = %d\n", wfx->wFormatTag); + dolog ("nChannels = %d\n", wfx->nChannels); + dolog ("nSamplesPerSec = %ld\n", wfx->nSamplesPerSec); + dolog ("nAvgBytesPerSec = %ld\n", wfx->nAvgBytesPerSec); + dolog ("nBlockAlign = %d\n", wfx->nBlockAlign); + dolog ("wBitsPerSample = %d\n", wfx->wBitsPerSample); + dolog ("cbSize = %d\n", wfx->cbSize); +} +#endif + +static int dsound_restore_out (LPDIRECTSOUNDBUFFER dsb, dsound *s) +{ + HRESULT hr; + + hr = IDirectSoundBuffer_Restore (dsb); + + if (hr != DS_OK) { + dsound_logerr (hr, "Could not restore playback buffer\n"); + return -1; + } + return 0; +} + +#include "dsound_template.h" +#define DSBTYPE_IN +#include "dsound_template.h" +#undef DSBTYPE_IN + +static int dsound_get_status_out (LPDIRECTSOUNDBUFFER dsb, DWORD *statusp, + dsound *s) +{ + HRESULT hr; + + hr = IDirectSoundBuffer_GetStatus (dsb, statusp); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not get playback buffer status\n"); + return -1; + } + + if (*statusp & DSBSTATUS_BUFFERLOST) { + dsound_restore_out(dsb, s); + return -1; + } + + return 0; +} + +static int dsound_get_status_in (LPDIRECTSOUNDCAPTUREBUFFER dscb, + DWORD *statusp) +{ + HRESULT hr; + + hr = IDirectSoundCaptureBuffer_GetStatus (dscb, statusp); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not get capture buffer status\n"); + return -1; + } + + return 0; +} + +static void dsound_clear_sample (HWVoiceOut *hw, LPDIRECTSOUNDBUFFER dsb, + dsound *s) +{ + int err; + LPVOID p1, p2; + DWORD blen1, blen2, len1, len2; + + err = dsound_lock_out ( + dsb, + &hw->info, + 0, + hw->size_emul, + &p1, &p2, + &blen1, &blen2, + 1, + s + ); + if (err) { + return; + } + + len1 = blen1 / hw->info.bytes_per_frame; + len2 = blen2 / hw->info.bytes_per_frame; + +#ifdef DEBUG_DSOUND + dolog ("clear %p,%ld,%ld %p,%ld,%ld\n", + p1, blen1, len1, + p2, blen2, len2); +#endif + + if (p1 && len1) { + audio_pcm_info_clear_buf (&hw->info, p1, len1); + } + + if (p2 && len2) { + audio_pcm_info_clear_buf (&hw->info, p2, len2); + } + + dsound_unlock_out (dsb, p1, p2, blen1, blen2); +} + +static int dsound_set_cooperative_level(dsound *s) +{ + HRESULT hr; + HWND hwnd; + + hwnd = GetDesktopWindow(); + hr = IDirectSound_SetCooperativeLevel ( + s->dsound, + hwnd, + DSSCL_PRIORITY + ); + + if (FAILED (hr)) { + dsound_logerr (hr, "Could not set cooperative level for window %p\n", + hwnd); + return -1; + } + + return 0; +} + +static void dsound_enable_out(HWVoiceOut *hw, bool enable) +{ + HRESULT hr; + DWORD status; + DSoundVoiceOut *ds = (DSoundVoiceOut *) hw; + LPDIRECTSOUNDBUFFER dsb = ds->dsound_buffer; + dsound *s = ds->s; + + if (!dsb) { + dolog ("Attempt to control voice without a buffer\n"); + return; + } + + if (enable) { + if (dsound_get_status_out (dsb, &status, s)) { + return; + } + + if (status & DSBSTATUS_PLAYING) { + dolog ("warning: Voice is already playing\n"); + return; + } + + dsound_clear_sample (hw, dsb, s); + + hr = IDirectSoundBuffer_Play (dsb, 0, 0, DSBPLAY_LOOPING); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not start playing buffer\n"); + return; + } + } else { + if (dsound_get_status_out (dsb, &status, s)) { + return; + } + + if (status & DSBSTATUS_PLAYING) { + hr = IDirectSoundBuffer_Stop (dsb); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not stop playing buffer\n"); + return; + } + } else { + dolog ("warning: Voice is not playing\n"); + } + } +} + +static void *dsound_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + DSoundVoiceOut *ds = (DSoundVoiceOut *) hw; + LPDIRECTSOUNDBUFFER dsb = ds->dsound_buffer; + HRESULT hr; + DWORD ppos, wpos, act_size; + size_t req_size; + int err; + void *ret; + + hr = IDirectSoundBuffer_GetCurrentPosition( + dsb, &ppos, ds->first_time ? &wpos : NULL); + if (FAILED(hr)) { + dsound_logerr(hr, "Could not get playback buffer position\n"); + *size = 0; + return NULL; + } + + if (ds->first_time) { + hw->pos_emul = wpos; + ds->first_time = false; + } + + req_size = audio_ring_dist(ppos, hw->pos_emul, hw->size_emul); + req_size = MIN(req_size, hw->size_emul - hw->pos_emul); + + if (req_size == 0) { + *size = 0; + return NULL; + } + + err = dsound_lock_out(dsb, &hw->info, hw->pos_emul, req_size, &ret, NULL, + &act_size, NULL, false, ds->s); + if (err) { + dolog("Failed to lock buffer\n"); + *size = 0; + return NULL; + } + + *size = act_size; + return ret; +} + +static size_t dsound_put_buffer_out(HWVoiceOut *hw, void *buf, size_t len) +{ + DSoundVoiceOut *ds = (DSoundVoiceOut *) hw; + LPDIRECTSOUNDBUFFER dsb = ds->dsound_buffer; + int err = dsound_unlock_out(dsb, buf, NULL, len, 0); + + if (err) { + dolog("Failed to unlock buffer!!\n"); + return 0; + } + hw->pos_emul = (hw->pos_emul + len) % hw->size_emul; + + return len; +} + +static void dsound_enable_in(HWVoiceIn *hw, bool enable) +{ + HRESULT hr; + DWORD status; + DSoundVoiceIn *ds = (DSoundVoiceIn *) hw; + LPDIRECTSOUNDCAPTUREBUFFER dscb = ds->dsound_capture_buffer; + + if (!dscb) { + dolog ("Attempt to control capture voice without a buffer\n"); + return; + } + + if (enable) { + if (dsound_get_status_in (dscb, &status)) { + return; + } + + if (status & DSCBSTATUS_CAPTURING) { + dolog ("warning: Voice is already capturing\n"); + return; + } + + /* clear ?? */ + + hr = IDirectSoundCaptureBuffer_Start (dscb, DSCBSTART_LOOPING); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not start capturing\n"); + return; + } + } else { + if (dsound_get_status_in (dscb, &status)) { + return; + } + + if (status & DSCBSTATUS_CAPTURING) { + hr = IDirectSoundCaptureBuffer_Stop (dscb); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not stop capturing\n"); + return; + } + } else { + dolog ("warning: Voice is not capturing\n"); + } + } +} + +static void *dsound_get_buffer_in(HWVoiceIn *hw, size_t *size) +{ + DSoundVoiceIn *ds = (DSoundVoiceIn *) hw; + LPDIRECTSOUNDCAPTUREBUFFER dscb = ds->dsound_capture_buffer; + HRESULT hr; + DWORD cpos, rpos, act_size; + size_t req_size; + int err; + void *ret; + + hr = IDirectSoundCaptureBuffer_GetCurrentPosition( + dscb, &cpos, ds->first_time ? &rpos : NULL); + if (FAILED(hr)) { + dsound_logerr(hr, "Could not get capture buffer position\n"); + *size = 0; + return NULL; + } + + if (ds->first_time) { + hw->pos_emul = rpos; + ds->first_time = false; + } + + req_size = audio_ring_dist(cpos, hw->pos_emul, hw->size_emul); + req_size = MIN(*size, MIN(req_size, hw->size_emul - hw->pos_emul)); + + if (req_size == 0) { + *size = 0; + return NULL; + } + + err = dsound_lock_in(dscb, &hw->info, hw->pos_emul, req_size, &ret, NULL, + &act_size, NULL, false, ds->s); + if (err) { + dolog("Failed to lock buffer\n"); + *size = 0; + return NULL; + } + + *size = act_size; + return ret; +} + +static void dsound_put_buffer_in(HWVoiceIn *hw, void *buf, size_t len) +{ + DSoundVoiceIn *ds = (DSoundVoiceIn *) hw; + LPDIRECTSOUNDCAPTUREBUFFER dscb = ds->dsound_capture_buffer; + int err = dsound_unlock_in(dscb, buf, NULL, len, 0); + + if (err) { + dolog("Failed to unlock buffer!!\n"); + return; + } + hw->pos_emul = (hw->pos_emul + len) % hw->size_emul; +} + +static void dsound_audio_fini (void *opaque) +{ + HRESULT hr; + dsound *s = opaque; + + if (!s->dsound) { + g_free(s); + return; + } + + hr = IDirectSound_Release (s->dsound); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not release DirectSound\n"); + } + s->dsound = NULL; + + if (!s->dsound_capture) { + g_free(s); + return; + } + + hr = IDirectSoundCapture_Release (s->dsound_capture); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not release DirectSoundCapture\n"); + } + s->dsound_capture = NULL; + + g_free(s); +} + +static void *dsound_audio_init(Audiodev *dev) +{ + int err; + HRESULT hr; + dsound *s = g_malloc0(sizeof(dsound)); + AudiodevDsoundOptions *dso; + + assert(dev->driver == AUDIODEV_DRIVER_DSOUND); + s->dev = dev; + dso = &dev->u.dsound; + + if (!dso->has_latency) { + dso->has_latency = true; + dso->latency = 10000; /* 10 ms */ + } + + hr = CoInitialize (NULL); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not initialize COM\n"); + g_free(s); + return NULL; + } + + hr = CoCreateInstance ( + &CLSID_DirectSound, + NULL, + CLSCTX_ALL, + &IID_IDirectSound, + (void **) &s->dsound + ); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not create DirectSound instance\n"); + g_free(s); + return NULL; + } + + hr = IDirectSound_Initialize (s->dsound, NULL); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not initialize DirectSound\n"); + + hr = IDirectSound_Release (s->dsound); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not release DirectSound\n"); + } + g_free(s); + return NULL; + } + + hr = CoCreateInstance ( + &CLSID_DirectSoundCapture, + NULL, + CLSCTX_ALL, + &IID_IDirectSoundCapture, + (void **) &s->dsound_capture + ); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not create DirectSoundCapture instance\n"); + } else { + hr = IDirectSoundCapture_Initialize (s->dsound_capture, NULL); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not initialize DirectSoundCapture\n"); + + hr = IDirectSoundCapture_Release (s->dsound_capture); + if (FAILED (hr)) { + dsound_logerr (hr, "Could not release DirectSoundCapture\n"); + } + s->dsound_capture = NULL; + } + } + + err = dsound_set_cooperative_level(s); + if (err) { + dsound_audio_fini (s); + return NULL; + } + + return s; +} + +static struct audio_pcm_ops dsound_pcm_ops = { + .init_out = dsound_init_out, + .fini_out = dsound_fini_out, + .write = audio_generic_write, + .get_buffer_out = dsound_get_buffer_out, + .put_buffer_out = dsound_put_buffer_out, + .enable_out = dsound_enable_out, + + .init_in = dsound_init_in, + .fini_in = dsound_fini_in, + .read = audio_generic_read, + .get_buffer_in = dsound_get_buffer_in, + .put_buffer_in = dsound_put_buffer_in, + .enable_in = dsound_enable_in, +}; + +static struct audio_driver dsound_audio_driver = { + .name = "dsound", + .descr = "DirectSound http://wikipedia.org/wiki/DirectSound", + .init = dsound_audio_init, + .fini = dsound_audio_fini, + .pcm_ops = &dsound_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = 1, + .voice_size_out = sizeof (DSoundVoiceOut), + .voice_size_in = sizeof (DSoundVoiceIn) +}; + +static void register_audio_dsound(void) +{ + audio_driver_register(&dsound_audio_driver); +} +type_init(register_audio_dsound); diff --git a/audio/jackaudio.c b/audio/jackaudio.c new file mode 100644 index 000000000..e7de6d543 --- /dev/null +++ b/audio/jackaudio.c @@ -0,0 +1,694 @@ +/* + * QEMU JACK Audio Connection Kit Client + * + * Copyright (c) 2020 Geoffrey McRae (gnif) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qemu/atomic.h" +#include "qemu/main-loop.h" +#include "audio.h" + +#define AUDIO_CAP "jack" +#include "audio_int.h" + +#include <jack/jack.h> +#include <jack/thread.h> + +struct QJack; + +typedef enum QJackState { + QJACK_STATE_DISCONNECTED, + QJACK_STATE_RUNNING, + QJACK_STATE_SHUTDOWN +} +QJackState; + +typedef struct QJackBuffer { + int channels; + int frames; + uint32_t used; + int rptr, wptr; + float **data; +} +QJackBuffer; + +typedef struct QJackClient { + AudiodevJackPerDirectionOptions *opt; + + bool out; + bool enabled; + bool connect_ports; + int packets; + + QJackState state; + jack_client_t *client; + jack_nframes_t freq; + QEMUBH *shutdown_bh; + + struct QJack *j; + int nchannels; + int buffersize; + jack_port_t **port; + QJackBuffer fifo; +} +QJackClient; + +typedef struct QJackOut { + HWVoiceOut hw; + QJackClient c; +} +QJackOut; + +typedef struct QJackIn { + HWVoiceIn hw; + QJackClient c; +} +QJackIn; + +static int qjack_client_init(QJackClient *c); +static void qjack_client_connect_ports(QJackClient *c); +static void qjack_client_fini(QJackClient *c); +static QemuMutex qjack_shutdown_lock; + +static void qjack_buffer_create(QJackBuffer *buffer, int channels, int frames) +{ + buffer->channels = channels; + buffer->frames = frames; + buffer->used = 0; + buffer->rptr = 0; + buffer->wptr = 0; + buffer->data = g_malloc(channels * sizeof(float *)); + for (int i = 0; i < channels; ++i) { + buffer->data[i] = g_malloc(frames * sizeof(float)); + } +} + +static void qjack_buffer_clear(QJackBuffer *buffer) +{ + assert(buffer->data); + qatomic_store_release(&buffer->used, 0); + buffer->rptr = 0; + buffer->wptr = 0; +} + +static void qjack_buffer_free(QJackBuffer *buffer) +{ + if (!buffer->data) { + return; + } + + for (int i = 0; i < buffer->channels; ++i) { + g_free(buffer->data[i]); + } + + g_free(buffer->data); + buffer->data = NULL; +} + +/* write PCM interleaved */ +static int qjack_buffer_write(QJackBuffer *buffer, float *data, int size) +{ + assert(buffer->data); + const int samples = size / sizeof(float); + int frames = samples / buffer->channels; + const int avail = buffer->frames - qatomic_load_acquire(&buffer->used); + + if (frames > avail) { + frames = avail; + } + + int copy = frames; + int wptr = buffer->wptr; + + while (copy) { + + for (int c = 0; c < buffer->channels; ++c) { + buffer->data[c][wptr] = *data++; + } + + if (++wptr == buffer->frames) { + wptr = 0; + } + + --copy; + } + + buffer->wptr = wptr; + + qatomic_add(&buffer->used, frames); + return frames * buffer->channels * sizeof(float); +}; + +/* write PCM linear */ +static int qjack_buffer_write_l(QJackBuffer *buffer, float **dest, int frames) +{ + assert(buffer->data); + const int avail = buffer->frames - qatomic_load_acquire(&buffer->used); + int wptr = buffer->wptr; + + if (frames > avail) { + frames = avail; + } + + int right = buffer->frames - wptr; + if (right > frames) { + right = frames; + } + + const int left = frames - right; + for (int c = 0; c < buffer->channels; ++c) { + memcpy(buffer->data[c] + wptr, dest[c] , right * sizeof(float)); + memcpy(buffer->data[c] , dest[c] + right, left * sizeof(float)); + } + + wptr += frames; + if (wptr >= buffer->frames) { + wptr -= buffer->frames; + } + buffer->wptr = wptr; + + qatomic_add(&buffer->used, frames); + return frames; +} + +/* read PCM interleaved */ +static int qjack_buffer_read(QJackBuffer *buffer, float *dest, int size) +{ + assert(buffer->data); + const int samples = size / sizeof(float); + int frames = samples / buffer->channels; + const int avail = qatomic_load_acquire(&buffer->used); + + if (frames > avail) { + frames = avail; + } + + int copy = frames; + int rptr = buffer->rptr; + + while (copy) { + + for (int c = 0; c < buffer->channels; ++c) { + *dest++ = buffer->data[c][rptr]; + } + + if (++rptr == buffer->frames) { + rptr = 0; + } + + --copy; + } + + buffer->rptr = rptr; + + qatomic_sub(&buffer->used, frames); + return frames * buffer->channels * sizeof(float); +} + +/* read PCM linear */ +static int qjack_buffer_read_l(QJackBuffer *buffer, float **dest, int frames) +{ + assert(buffer->data); + int copy = frames; + const int used = qatomic_load_acquire(&buffer->used); + int rptr = buffer->rptr; + + if (copy > used) { + copy = used; + } + + int right = buffer->frames - rptr; + if (right > copy) { + right = copy; + } + + const int left = copy - right; + for (int c = 0; c < buffer->channels; ++c) { + memcpy(dest[c] , buffer->data[c] + rptr, right * sizeof(float)); + memcpy(dest[c] + right, buffer->data[c] , left * sizeof(float)); + } + + rptr += copy; + if (rptr >= buffer->frames) { + rptr -= buffer->frames; + } + buffer->rptr = rptr; + + qatomic_sub(&buffer->used, copy); + return copy; +} + +static int qjack_process(jack_nframes_t nframes, void *arg) +{ + QJackClient *c = (QJackClient *)arg; + + if (c->state != QJACK_STATE_RUNNING) { + return 0; + } + + /* get the buffers for the ports */ + float *buffers[c->nchannels]; + for (int i = 0; i < c->nchannels; ++i) { + buffers[i] = jack_port_get_buffer(c->port[i], nframes); + } + + if (c->out) { + if (likely(c->enabled)) { + qjack_buffer_read_l(&c->fifo, buffers, nframes); + } else { + for (int i = 0; i < c->nchannels; ++i) { + memset(buffers[i], 0, nframes * sizeof(float)); + } + } + } else { + if (likely(c->enabled)) { + qjack_buffer_write_l(&c->fifo, buffers, nframes); + } + } + + return 0; +} + +static void qjack_port_registration(jack_port_id_t port, int reg, void *arg) +{ + if (reg) { + QJackClient *c = (QJackClient *)arg; + c->connect_ports = true; + } +} + +static int qjack_xrun(void *arg) +{ + QJackClient *c = (QJackClient *)arg; + if (c->state != QJACK_STATE_RUNNING) { + return 0; + } + + qjack_buffer_clear(&c->fifo); + return 0; +} + +static void qjack_shutdown_bh(void *opaque) +{ + QJackClient *c = (QJackClient *)opaque; + qjack_client_fini(c); +} + +static void qjack_shutdown(void *arg) +{ + QJackClient *c = (QJackClient *)arg; + c->state = QJACK_STATE_SHUTDOWN; + qemu_bh_schedule(c->shutdown_bh); +} + +static void qjack_client_recover(QJackClient *c) +{ + if (c->state != QJACK_STATE_DISCONNECTED) { + return; + } + + /* packets is used simply to throttle this */ + if (c->packets % 100 == 0) { + + /* if enabled then attempt to recover */ + if (c->enabled) { + dolog("attempting to reconnect to server\n"); + qjack_client_init(c); + } + } +} + +static size_t qjack_write(HWVoiceOut *hw, void *buf, size_t len) +{ + QJackOut *jo = (QJackOut *)hw; + ++jo->c.packets; + + if (jo->c.state != QJACK_STATE_RUNNING) { + qjack_client_recover(&jo->c); + return len; + } + + qjack_client_connect_ports(&jo->c); + return qjack_buffer_write(&jo->c.fifo, buf, len); +} + +static size_t qjack_read(HWVoiceIn *hw, void *buf, size_t len) +{ + QJackIn *ji = (QJackIn *)hw; + ++ji->c.packets; + + if (ji->c.state != QJACK_STATE_RUNNING) { + qjack_client_recover(&ji->c); + return len; + } + + qjack_client_connect_ports(&ji->c); + return qjack_buffer_read(&ji->c.fifo, buf, len); +} + +static void qjack_client_connect_ports(QJackClient *c) +{ + if (!c->connect_ports || !c->opt->connect_ports) { + return; + } + + c->connect_ports = false; + const char **ports; + ports = jack_get_ports(c->client, c->opt->connect_ports, NULL, + c->out ? JackPortIsInput : JackPortIsOutput); + + if (!ports) { + return; + } + + for (int i = 0; i < c->nchannels && ports[i]; ++i) { + const char *p = jack_port_name(c->port[i]); + if (jack_port_connected_to(c->port[i], ports[i])) { + continue; + } + + if (c->out) { + dolog("connect %s -> %s\n", p, ports[i]); + jack_connect(c->client, p, ports[i]); + } else { + dolog("connect %s -> %s\n", ports[i], p); + jack_connect(c->client, ports[i], p); + } + } +} + +static int qjack_client_init(QJackClient *c) +{ + jack_status_t status; + char client_name[jack_client_name_size()]; + jack_options_t options = JackNullOption; + + if (c->state == QJACK_STATE_RUNNING) { + return 0; + } + + c->connect_ports = true; + + snprintf(client_name, sizeof(client_name), "%s-%s", + c->out ? "out" : "in", + c->opt->client_name ? c->opt->client_name : audio_application_name()); + + if (c->opt->exact_name) { + options |= JackUseExactName; + } + + if (!c->opt->start_server) { + options |= JackNoStartServer; + } + + if (c->opt->server_name) { + options |= JackServerName; + } + + c->client = jack_client_open(client_name, options, &status, + c->opt->server_name); + + if (c->client == NULL) { + dolog("jack_client_open failed: status = 0x%2.0x\n", status); + if (status & JackServerFailed) { + dolog("unable to connect to JACK server\n"); + } + return -1; + } + + c->freq = jack_get_sample_rate(c->client); + + if (status & JackServerStarted) { + dolog("JACK server started\n"); + } + + if (status & JackNameNotUnique) { + dolog("JACK unique name assigned %s\n", + jack_get_client_name(c->client)); + } + + jack_set_process_callback(c->client, qjack_process , c); + jack_set_port_registration_callback(c->client, qjack_port_registration, c); + jack_set_xrun_callback(c->client, qjack_xrun, c); + jack_on_shutdown(c->client, qjack_shutdown, c); + + /* allocate and register the ports */ + c->port = g_malloc(sizeof(jack_port_t *) * c->nchannels); + for (int i = 0; i < c->nchannels; ++i) { + + char port_name[16]; + snprintf( + port_name, + sizeof(port_name), + c->out ? "output %d" : "input %d", + i); + + c->port[i] = jack_port_register( + c->client, + port_name, + JACK_DEFAULT_AUDIO_TYPE, + c->out ? JackPortIsOutput : JackPortIsInput, + 0); + } + + /* activate the session */ + jack_activate(c->client); + c->buffersize = jack_get_buffer_size(c->client); + + /* + * ensure the buffersize is no smaller then 512 samples, some (all?) qemu + * virtual devices do not work correctly otherwise + */ + if (c->buffersize < 512) { + c->buffersize = 512; + } + + /* create a 2 period buffer */ + qjack_buffer_create(&c->fifo, c->nchannels, c->buffersize * 2); + + qjack_client_connect_ports(c); + c->state = QJACK_STATE_RUNNING; + return 0; +} + +static int qjack_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + QJackOut *jo = (QJackOut *)hw; + Audiodev *dev = (Audiodev *)drv_opaque; + + jo->c.out = true; + jo->c.enabled = false; + jo->c.nchannels = as->nchannels; + jo->c.opt = dev->u.jack.out; + + jo->c.shutdown_bh = qemu_bh_new(qjack_shutdown_bh, &jo->c); + + int ret = qjack_client_init(&jo->c); + if (ret != 0) { + qemu_bh_delete(jo->c.shutdown_bh); + return ret; + } + + /* report the buffer size to qemu */ + hw->samples = jo->c.buffersize; + + /* report the audio format we support */ + struct audsettings os = { + .freq = jo->c.freq, + .nchannels = jo->c.nchannels, + .fmt = AUDIO_FORMAT_F32, + .endianness = 0 + }; + audio_pcm_init_info(&hw->info, &os); + + dolog("JACK output configured for %dHz (%d samples)\n", + jo->c.freq, jo->c.buffersize); + + return 0; +} + +static int qjack_init_in(HWVoiceIn *hw, struct audsettings *as, + void *drv_opaque) +{ + QJackIn *ji = (QJackIn *)hw; + Audiodev *dev = (Audiodev *)drv_opaque; + + ji->c.out = false; + ji->c.enabled = false; + ji->c.nchannels = as->nchannels; + ji->c.opt = dev->u.jack.in; + + ji->c.shutdown_bh = qemu_bh_new(qjack_shutdown_bh, &ji->c); + + int ret = qjack_client_init(&ji->c); + if (ret != 0) { + qemu_bh_delete(ji->c.shutdown_bh); + return ret; + } + + /* report the buffer size to qemu */ + hw->samples = ji->c.buffersize; + + /* report the audio format we support */ + struct audsettings is = { + .freq = ji->c.freq, + .nchannels = ji->c.nchannels, + .fmt = AUDIO_FORMAT_F32, + .endianness = 0 + }; + audio_pcm_init_info(&hw->info, &is); + + dolog("JACK input configured for %dHz (%d samples)\n", + ji->c.freq, ji->c.buffersize); + + return 0; +} + +static void qjack_client_fini_locked(QJackClient *c) +{ + switch (c->state) { + case QJACK_STATE_RUNNING: + jack_deactivate(c->client); + /* fallthrough */ + + case QJACK_STATE_SHUTDOWN: + jack_client_close(c->client); + c->client = NULL; + + qjack_buffer_free(&c->fifo); + g_free(c->port); + + c->state = QJACK_STATE_DISCONNECTED; + /* fallthrough */ + + case QJACK_STATE_DISCONNECTED: + break; + } +} + +static void qjack_client_fini(QJackClient *c) +{ + qemu_mutex_lock(&qjack_shutdown_lock); + qjack_client_fini_locked(c); + qemu_mutex_unlock(&qjack_shutdown_lock); +} + +static void qjack_fini_out(HWVoiceOut *hw) +{ + QJackOut *jo = (QJackOut *)hw; + qjack_client_fini(&jo->c); + + qemu_bh_delete(jo->c.shutdown_bh); +} + +static void qjack_fini_in(HWVoiceIn *hw) +{ + QJackIn *ji = (QJackIn *)hw; + qjack_client_fini(&ji->c); + + qemu_bh_delete(ji->c.shutdown_bh); +} + +static void qjack_enable_out(HWVoiceOut *hw, bool enable) +{ + QJackOut *jo = (QJackOut *)hw; + jo->c.enabled = enable; +} + +static void qjack_enable_in(HWVoiceIn *hw, bool enable) +{ + QJackIn *ji = (QJackIn *)hw; + ji->c.enabled = enable; +} + +static int qjack_thread_creator(jack_native_thread_t *thread, + const pthread_attr_t *attr, void *(*function)(void *), void *arg) +{ + int ret = pthread_create(thread, attr, function, arg); + if (ret != 0) { + return ret; + } + + /* set the name of the thread */ + pthread_setname_np(*thread, "jack-client"); + + return ret; +} + +static void *qjack_init(Audiodev *dev) +{ + assert(dev->driver == AUDIODEV_DRIVER_JACK); + return dev; +} + +static void qjack_fini(void *opaque) +{ +} + +static struct audio_pcm_ops jack_pcm_ops = { + .init_out = qjack_init_out, + .fini_out = qjack_fini_out, + .write = qjack_write, + .run_buffer_out = audio_generic_run_buffer_out, + .enable_out = qjack_enable_out, + + .init_in = qjack_init_in, + .fini_in = qjack_fini_in, + .read = qjack_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = qjack_enable_in +}; + +static struct audio_driver jack_driver = { + .name = "jack", + .descr = "JACK Audio Connection Kit Client", + .init = qjack_init, + .fini = qjack_fini, + .pcm_ops = &jack_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof(QJackOut), + .voice_size_in = sizeof(QJackIn) +}; + +static void qjack_error(const char *msg) +{ + dolog("E: %s\n", msg); +} + +static void qjack_info(const char *msg) +{ + dolog("I: %s\n", msg); +} + +static void register_audio_jack(void) +{ + qemu_mutex_init(&qjack_shutdown_lock); + audio_driver_register(&jack_driver); + jack_set_thread_creator(qjack_thread_creator); + jack_set_error_function(qjack_error); + jack_set_info_function(qjack_info); +} +type_init(register_audio_jack); diff --git a/audio/meson.build b/audio/meson.build new file mode 100644 index 000000000..462533bb8 --- /dev/null +++ b/audio/meson.build @@ -0,0 +1,29 @@ +softmmu_ss.add([spice_headers, files('audio.c')]) +softmmu_ss.add(files( + 'audio_legacy.c', + 'mixeng.c', + 'noaudio.c', + 'wavaudio.c', + 'wavcapture.c', +)) + +softmmu_ss.add(when: coreaudio, if_true: files('coreaudio.c')) +softmmu_ss.add(when: dsound, if_true: files('dsoundaudio.c', 'audio_win_int.c')) + +audio_modules = {} +foreach m : [ + ['alsa', alsa, files('alsaaudio.c')], + ['oss', oss, files('ossaudio.c')], + ['pa', pulse, files('paaudio.c')], + ['sdl', sdl, files('sdlaudio.c')], + ['jack', jack, files('jackaudio.c')], + ['spice', spice, files('spiceaudio.c')] +] + if m[1].found() + module_ss = ss.source_set() + module_ss.add(m[1], m[2]) + audio_modules += {m[0] : module_ss} + endif +endforeach + +modules += {'audio': audio_modules} diff --git a/audio/mixeng.c b/audio/mixeng.c new file mode 100644 index 000000000..f27deb165 --- /dev/null +++ b/audio/mixeng.c @@ -0,0 +1,470 @@ +/* + * QEMU Mixing engine + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * Copyright (c) 1998 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" +#include "qemu/bswap.h" +#include "qemu/error-report.h" +#include "audio.h" + +#define AUDIO_CAP "mixeng" +#include "audio_int.h" + +/* 8 bit */ +#define ENDIAN_CONVERSION natural +#define ENDIAN_CONVERT(v) (v) + +/* Signed 8 bit */ +#define BSIZE 8 +#define ITYPE int +#define IN_MIN SCHAR_MIN +#define IN_MAX SCHAR_MAX +#define SIGNED +#define SHIFT 8 +#include "mixeng_template.h" +#undef SIGNED +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +/* Unsigned 8 bit */ +#define BSIZE 8 +#define ITYPE uint +#define IN_MIN 0 +#define IN_MAX UCHAR_MAX +#define SHIFT 8 +#include "mixeng_template.h" +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION + +/* Signed 16 bit */ +#define BSIZE 16 +#define ITYPE int +#define IN_MIN SHRT_MIN +#define IN_MAX SHRT_MAX +#define SIGNED +#define SHIFT 16 +#define ENDIAN_CONVERSION natural +#define ENDIAN_CONVERT(v) (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#define ENDIAN_CONVERSION swap +#define ENDIAN_CONVERT(v) bswap16 (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#undef SIGNED +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +/* Unsigned 16 bit */ +#define BSIZE 16 +#define ITYPE uint +#define IN_MIN 0 +#define IN_MAX USHRT_MAX +#define SHIFT 16 +#define ENDIAN_CONVERSION natural +#define ENDIAN_CONVERT(v) (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#define ENDIAN_CONVERSION swap +#define ENDIAN_CONVERT(v) bswap16 (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +/* Signed 32 bit */ +#define BSIZE 32 +#define ITYPE int +#define IN_MIN INT32_MIN +#define IN_MAX INT32_MAX +#define SIGNED +#define SHIFT 32 +#define ENDIAN_CONVERSION natural +#define ENDIAN_CONVERT(v) (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#define ENDIAN_CONVERSION swap +#define ENDIAN_CONVERT(v) bswap32 (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#undef SIGNED +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +/* Unsigned 32 bit */ +#define BSIZE 32 +#define ITYPE uint +#define IN_MIN 0 +#define IN_MAX UINT32_MAX +#define SHIFT 32 +#define ENDIAN_CONVERSION natural +#define ENDIAN_CONVERT(v) (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#define ENDIAN_CONVERSION swap +#define ENDIAN_CONVERT(v) bswap32 (v) +#include "mixeng_template.h" +#undef ENDIAN_CONVERT +#undef ENDIAN_CONVERSION +#undef IN_MAX +#undef IN_MIN +#undef BSIZE +#undef ITYPE +#undef SHIFT + +t_sample *mixeng_conv[2][2][2][3] = { + { + { + { + conv_natural_uint8_t_to_mono, + conv_natural_uint16_t_to_mono, + conv_natural_uint32_t_to_mono + }, + { + conv_natural_uint8_t_to_mono, + conv_swap_uint16_t_to_mono, + conv_swap_uint32_t_to_mono, + } + }, + { + { + conv_natural_int8_t_to_mono, + conv_natural_int16_t_to_mono, + conv_natural_int32_t_to_mono + }, + { + conv_natural_int8_t_to_mono, + conv_swap_int16_t_to_mono, + conv_swap_int32_t_to_mono + } + } + }, + { + { + { + conv_natural_uint8_t_to_stereo, + conv_natural_uint16_t_to_stereo, + conv_natural_uint32_t_to_stereo + }, + { + conv_natural_uint8_t_to_stereo, + conv_swap_uint16_t_to_stereo, + conv_swap_uint32_t_to_stereo + } + }, + { + { + conv_natural_int8_t_to_stereo, + conv_natural_int16_t_to_stereo, + conv_natural_int32_t_to_stereo + }, + { + conv_natural_int8_t_to_stereo, + conv_swap_int16_t_to_stereo, + conv_swap_int32_t_to_stereo, + } + } + } +}; + +f_sample *mixeng_clip[2][2][2][3] = { + { + { + { + clip_natural_uint8_t_from_mono, + clip_natural_uint16_t_from_mono, + clip_natural_uint32_t_from_mono + }, + { + clip_natural_uint8_t_from_mono, + clip_swap_uint16_t_from_mono, + clip_swap_uint32_t_from_mono + } + }, + { + { + clip_natural_int8_t_from_mono, + clip_natural_int16_t_from_mono, + clip_natural_int32_t_from_mono + }, + { + clip_natural_int8_t_from_mono, + clip_swap_int16_t_from_mono, + clip_swap_int32_t_from_mono + } + } + }, + { + { + { + clip_natural_uint8_t_from_stereo, + clip_natural_uint16_t_from_stereo, + clip_natural_uint32_t_from_stereo + }, + { + clip_natural_uint8_t_from_stereo, + clip_swap_uint16_t_from_stereo, + clip_swap_uint32_t_from_stereo + } + }, + { + { + clip_natural_int8_t_from_stereo, + clip_natural_int16_t_from_stereo, + clip_natural_int32_t_from_stereo + }, + { + clip_natural_int8_t_from_stereo, + clip_swap_int16_t_from_stereo, + clip_swap_int32_t_from_stereo + } + } + } +}; + +#ifdef FLOAT_MIXENG +#define CONV_NATURAL_FLOAT(x) (x) +#define CLIP_NATURAL_FLOAT(x) (x) +#else +/* macros to map [-1.f, 1.f] <-> [INT32_MIN, INT32_MAX + 1] */ +static const float float_scale = (int64_t)INT32_MAX + 1; +#define CONV_NATURAL_FLOAT(x) ((x) * float_scale) + +#ifdef RECIPROCAL +static const float float_scale_reciprocal = 1.f / ((int64_t)INT32_MAX + 1); +#define CLIP_NATURAL_FLOAT(x) ((x) * float_scale_reciprocal) +#else +#define CLIP_NATURAL_FLOAT(x) ((x) / float_scale) +#endif +#endif + +static void conv_natural_float_to_mono(struct st_sample *dst, const void *src, + int samples) +{ + float *in = (float *)src; + + while (samples--) { + dst->r = dst->l = CONV_NATURAL_FLOAT(*in++); + dst++; + } +} + +static void conv_natural_float_to_stereo(struct st_sample *dst, const void *src, + int samples) +{ + float *in = (float *)src; + + while (samples--) { + dst->l = CONV_NATURAL_FLOAT(*in++); + dst->r = CONV_NATURAL_FLOAT(*in++); + dst++; + } +} + +t_sample *mixeng_conv_float[2] = { + conv_natural_float_to_mono, + conv_natural_float_to_stereo, +}; + +static void clip_natural_float_from_mono(void *dst, const struct st_sample *src, + int samples) +{ + float *out = (float *)dst; + + while (samples--) { + *out++ = CLIP_NATURAL_FLOAT(src->l + src->r); + src++; + } +} + +static void clip_natural_float_from_stereo( + void *dst, const struct st_sample *src, int samples) +{ + float *out = (float *)dst; + + while (samples--) { + *out++ = CLIP_NATURAL_FLOAT(src->l); + *out++ = CLIP_NATURAL_FLOAT(src->r); + src++; + } +} + +f_sample *mixeng_clip_float[2] = { + clip_natural_float_from_mono, + clip_natural_float_from_stereo, +}; + +void audio_sample_to_uint64(const void *samples, int pos, + uint64_t *left, uint64_t *right) +{ + const struct st_sample *sample = samples; + sample += pos; +#ifdef FLOAT_MIXENG + error_report( + "Coreaudio and floating point samples are not supported by replay yet"); + abort(); +#else + *left = sample->l; + *right = sample->r; +#endif +} + +void audio_sample_from_uint64(void *samples, int pos, + uint64_t left, uint64_t right) +{ + struct st_sample *sample = samples; + sample += pos; +#ifdef FLOAT_MIXENG + error_report( + "Coreaudio and floating point samples are not supported by replay yet"); + abort(); +#else + sample->l = left; + sample->r = right; +#endif +} + +/* + * August 21, 1998 + * Copyright 1998 Fabrice Bellard. + * + * [Rewrote completely the code of Lance Norskog And Sundry + * Contributors with a more efficient algorithm.] + * + * This source code is freely redistributable and may be used for + * any purpose. This copyright notice must be maintained. + * Lance Norskog And Sundry Contributors are not responsible for + * the consequences of using this software. + */ + +/* + * Sound Tools rate change effect file. + */ +/* + * Linear Interpolation. + * + * The use of fractional increment allows us to use no buffer. It + * avoid the problems at the end of the buffer we had with the old + * method which stored a possibly big buffer of size + * lcm(in_rate,out_rate). + * + * Limited to 16 bit samples and sampling frequency <= 65535 Hz. If + * the input & output frequencies are equal, a delay of one sample is + * introduced. Limited to processing 32-bit count worth of samples. + * + * 1 << FRAC_BITS evaluating to zero in several places. Changed with + * an (unsigned long) cast to make it safe. MarkMLl 2/1/99 + */ + +/* Private data */ +struct rate { + uint64_t opos; + uint64_t opos_inc; + uint32_t ipos; /* position in the input stream (integer) */ + struct st_sample ilast; /* last sample in the input stream */ +}; + +/* + * Prepare processing. + */ +void *st_rate_start (int inrate, int outrate) +{ + struct rate *rate = audio_calloc(__func__, 1, sizeof(*rate)); + + if (!rate) { + dolog ("Could not allocate resampler (%zu bytes)\n", sizeof (*rate)); + return NULL; + } + + rate->opos = 0; + + /* increment */ + rate->opos_inc = ((uint64_t) inrate << 32) / outrate; + + rate->ipos = 0; + rate->ilast.l = 0; + rate->ilast.r = 0; + return rate; +} + +#define NAME st_rate_flow_mix +#define OP(a, b) a += b +#include "rate_template.h" + +#define NAME st_rate_flow +#define OP(a, b) a = b +#include "rate_template.h" + +void st_rate_stop (void *opaque) +{ + g_free (opaque); +} + +void mixeng_clear (struct st_sample *buf, int len) +{ + memset (buf, 0, len * sizeof (struct st_sample)); +} + +void mixeng_volume (struct st_sample *buf, int len, struct mixeng_volume *vol) +{ + if (vol->mute) { + mixeng_clear (buf, len); + return; + } + + while (len--) { +#ifdef FLOAT_MIXENG + buf->l = buf->l * vol->l; + buf->r = buf->r * vol->r; +#else + buf->l = (buf->l * vol->l) >> 32; + buf->r = (buf->r * vol->r) >> 32; +#endif + buf += 1; + } +} diff --git a/audio/mixeng.h b/audio/mixeng.h new file mode 100644 index 000000000..2dcd6df24 --- /dev/null +++ b/audio/mixeng.h @@ -0,0 +1,58 @@ +/* + * QEMU Mixing engine header + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef QEMU_MIXENG_H +#define QEMU_MIXENG_H + +#ifdef FLOAT_MIXENG +typedef float mixeng_real; +struct mixeng_volume { int mute; mixeng_real r; mixeng_real l; }; +struct st_sample { mixeng_real l; mixeng_real r; }; +#else +struct mixeng_volume { int mute; int64_t r; int64_t l; }; +struct st_sample { int64_t l; int64_t r; }; +#endif +typedef struct st_sample st_sample; + +typedef void (t_sample) (struct st_sample *dst, const void *src, int samples); +typedef void (f_sample) (void *dst, const struct st_sample *src, int samples); + +/* indices: [stereo][signed][swap endiannes][8, 16 or 32-bits] */ +extern t_sample *mixeng_conv[2][2][2][3]; +extern f_sample *mixeng_clip[2][2][2][3]; + +/* indices: [stereo] */ +extern t_sample *mixeng_conv_float[2]; +extern f_sample *mixeng_clip_float[2]; + +void *st_rate_start (int inrate, int outrate); +void st_rate_flow(void *opaque, st_sample *ibuf, st_sample *obuf, + size_t *isamp, size_t *osamp); +void st_rate_flow_mix(void *opaque, st_sample *ibuf, st_sample *obuf, + size_t *isamp, size_t *osamp); +void st_rate_stop (void *opaque); +void mixeng_clear (struct st_sample *buf, int len); +void mixeng_volume (struct st_sample *buf, int len, struct mixeng_volume *vol); + +#endif /* QEMU_MIXENG_H */ diff --git a/audio/mixeng_template.h b/audio/mixeng_template.h new file mode 100644 index 000000000..bc8509e42 --- /dev/null +++ b/audio/mixeng_template.h @@ -0,0 +1,152 @@ +/* + * QEMU Mixing engine + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * Tusen tack till Mike Nordell + * dec++'ified by Dscho + */ + +#ifndef SIGNED +#define HALF (IN_MAX >> 1) +#endif + +#define ET glue (ENDIAN_CONVERSION, glue (glue (glue (_, ITYPE), BSIZE), _t)) +#define IN_T glue (glue (ITYPE, BSIZE), _t) + +#ifdef FLOAT_MIXENG +static inline mixeng_real glue (conv_, ET) (IN_T v) +{ + IN_T nv = ENDIAN_CONVERT (v); + +#ifdef RECIPROCAL +#ifdef SIGNED + return nv * (2.f / ((mixeng_real)IN_MAX - IN_MIN)); +#else + return (nv - HALF) * (2.f / (mixeng_real)IN_MAX); +#endif +#else /* !RECIPROCAL */ +#ifdef SIGNED + return nv / (((mixeng_real)IN_MAX - IN_MIN) / 2.f); +#else + return (nv - HALF) / ((mixeng_real)IN_MAX / 2.f); +#endif +#endif +} + +static inline IN_T glue (clip_, ET) (mixeng_real v) +{ + if (v >= 1.f) { + return IN_MAX; + } else if (v < -1.f) { + return IN_MIN; + } + +#ifdef SIGNED + return ENDIAN_CONVERT((IN_T)(v * (((mixeng_real)IN_MAX - IN_MIN) / 2.f))); +#else + return ENDIAN_CONVERT((IN_T)((v * ((mixeng_real)IN_MAX / 2.f)) + HALF)); +#endif +} + +#else /* !FLOAT_MIXENG */ + +static inline int64_t glue (conv_, ET) (IN_T v) +{ + IN_T nv = ENDIAN_CONVERT (v); +#ifdef SIGNED + return ((int64_t) nv) << (32 - SHIFT); +#else + return ((int64_t) nv - HALF) << (32 - SHIFT); +#endif +} + +static inline IN_T glue (clip_, ET) (int64_t v) +{ + if (v >= 0x7fffffffLL) { + return IN_MAX; + } else if (v < -2147483648LL) { + return IN_MIN; + } + +#ifdef SIGNED + return ENDIAN_CONVERT ((IN_T) (v >> (32 - SHIFT))); +#else + return ENDIAN_CONVERT ((IN_T) ((v >> (32 - SHIFT)) + HALF)); +#endif +} +#endif + +static void glue (glue (conv_, ET), _to_stereo) + (struct st_sample *dst, const void *src, int samples) +{ + struct st_sample *out = dst; + IN_T *in = (IN_T *) src; + + while (samples--) { + out->l = glue (conv_, ET) (*in++); + out->r = glue (conv_, ET) (*in++); + out += 1; + } +} + +static void glue (glue (conv_, ET), _to_mono) + (struct st_sample *dst, const void *src, int samples) +{ + struct st_sample *out = dst; + IN_T *in = (IN_T *) src; + + while (samples--) { + out->l = glue (conv_, ET) (in[0]); + out->r = out->l; + out += 1; + in += 1; + } +} + +static void glue (glue (clip_, ET), _from_stereo) + (void *dst, const struct st_sample *src, int samples) +{ + const struct st_sample *in = src; + IN_T *out = (IN_T *) dst; + while (samples--) { + *out++ = glue (clip_, ET) (in->l); + *out++ = glue (clip_, ET) (in->r); + in += 1; + } +} + +static void glue (glue (clip_, ET), _from_mono) + (void *dst, const struct st_sample *src, int samples) +{ + const struct st_sample *in = src; + IN_T *out = (IN_T *) dst; + while (samples--) { + *out++ = glue (clip_, ET) (in->l + in->r); + in += 1; + } +} + +#undef ET +#undef HALF +#undef IN_T diff --git a/audio/noaudio.c b/audio/noaudio.c new file mode 100644 index 000000000..aac87dbc9 --- /dev/null +++ b/audio/noaudio.c @@ -0,0 +1,148 @@ +/* + * QEMU Timer based audio emulation + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" +#include "audio.h" +#include "qemu/timer.h" + +#define AUDIO_CAP "noaudio" +#include "audio_int.h" + +typedef struct NoVoiceOut { + HWVoiceOut hw; + RateCtl rate; +} NoVoiceOut; + +typedef struct NoVoiceIn { + HWVoiceIn hw; + RateCtl rate; +} NoVoiceIn; + +static size_t no_write(HWVoiceOut *hw, void *buf, size_t len) +{ + NoVoiceOut *no = (NoVoiceOut *) hw; + return audio_rate_get_bytes(&hw->info, &no->rate, len); +} + +static int no_init_out(HWVoiceOut *hw, struct audsettings *as, void *drv_opaque) +{ + NoVoiceOut *no = (NoVoiceOut *) hw; + + audio_pcm_init_info (&hw->info, as); + hw->samples = 1024; + audio_rate_start(&no->rate); + return 0; +} + +static void no_fini_out (HWVoiceOut *hw) +{ + (void) hw; +} + +static void no_enable_out(HWVoiceOut *hw, bool enable) +{ + NoVoiceOut *no = (NoVoiceOut *) hw; + + if (enable) { + audio_rate_start(&no->rate); + } +} + +static int no_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + NoVoiceIn *no = (NoVoiceIn *) hw; + + audio_pcm_init_info (&hw->info, as); + hw->samples = 1024; + audio_rate_start(&no->rate); + return 0; +} + +static void no_fini_in (HWVoiceIn *hw) +{ + (void) hw; +} + +static size_t no_read(HWVoiceIn *hw, void *buf, size_t size) +{ + NoVoiceIn *no = (NoVoiceIn *) hw; + int64_t bytes = audio_rate_get_bytes(&hw->info, &no->rate, size); + + audio_pcm_info_clear_buf(&hw->info, buf, bytes / hw->info.bytes_per_frame); + return bytes; +} + +static void no_enable_in(HWVoiceIn *hw, bool enable) +{ + NoVoiceIn *no = (NoVoiceIn *) hw; + + if (enable) { + audio_rate_start(&no->rate); + } +} + +static void *no_audio_init(Audiodev *dev) +{ + return &no_audio_init; +} + +static void no_audio_fini (void *opaque) +{ + (void) opaque; +} + +static struct audio_pcm_ops no_pcm_ops = { + .init_out = no_init_out, + .fini_out = no_fini_out, + .write = no_write, + .run_buffer_out = audio_generic_run_buffer_out, + .enable_out = no_enable_out, + + .init_in = no_init_in, + .fini_in = no_fini_in, + .read = no_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = no_enable_in +}; + +static struct audio_driver no_audio_driver = { + .name = "none", + .descr = "Timer based audio emulation", + .init = no_audio_init, + .fini = no_audio_fini, + .pcm_ops = &no_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof (NoVoiceOut), + .voice_size_in = sizeof (NoVoiceIn) +}; + +static void register_audio_none(void) +{ + audio_driver_register(&no_audio_driver); +} +type_init(register_audio_none); diff --git a/audio/ossaudio.c b/audio/ossaudio.c new file mode 100644 index 000000000..60eff6642 --- /dev/null +++ b/audio/ossaudio.c @@ -0,0 +1,782 @@ +/* + * QEMU OSS audio driver + * + * Copyright (c) 2003-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include <sys/ioctl.h> +#include <sys/soundcard.h> +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qemu/host-utils.h" +#include "audio.h" +#include "trace.h" + +#define AUDIO_CAP "oss" +#include "audio_int.h" + +#if defined OSS_GETVERSION && defined SNDCTL_DSP_POLICY +#define USE_DSP_POLICY +#endif + +typedef struct OSSVoiceOut { + HWVoiceOut hw; + int fd; + int nfrags; + int fragsize; + int mmapped; + Audiodev *dev; +} OSSVoiceOut; + +typedef struct OSSVoiceIn { + HWVoiceIn hw; + int fd; + int nfrags; + int fragsize; + Audiodev *dev; +} OSSVoiceIn; + +struct oss_params { + int freq; + int fmt; + int nchannels; + int nfrags; + int fragsize; +}; + +static void GCC_FMT_ATTR (2, 3) oss_logerr (int err, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", strerror (err)); +} + +static void GCC_FMT_ATTR (3, 4) oss_logerr2 ( + int err, + const char *typ, + const char *fmt, + ... + ) +{ + va_list ap; + + AUD_log (AUDIO_CAP, "Could not initialize %s\n", typ); + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", strerror (err)); +} + +static void oss_anal_close (int *fdp) +{ + int err; + + qemu_set_fd_handler (*fdp, NULL, NULL, NULL); + err = close (*fdp); + if (err) { + oss_logerr (errno, "Failed to close file(fd=%d)\n", *fdp); + } + *fdp = -1; +} + +static void oss_helper_poll_out (void *opaque) +{ + AudioState *s = opaque; + audio_run(s, "oss_poll_out"); +} + +static void oss_helper_poll_in (void *opaque) +{ + AudioState *s = opaque; + audio_run(s, "oss_poll_in"); +} + +static void oss_poll_out (HWVoiceOut *hw) +{ + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + + qemu_set_fd_handler(oss->fd, NULL, oss_helper_poll_out, hw->s); +} + +static void oss_poll_in (HWVoiceIn *hw) +{ + OSSVoiceIn *oss = (OSSVoiceIn *) hw; + + qemu_set_fd_handler(oss->fd, oss_helper_poll_in, NULL, hw->s); +} + +static int aud_to_ossfmt (AudioFormat fmt, int endianness) +{ + switch (fmt) { + case AUDIO_FORMAT_S8: + return AFMT_S8; + + case AUDIO_FORMAT_U8: + return AFMT_U8; + + case AUDIO_FORMAT_S16: + if (endianness) { + return AFMT_S16_BE; + } else { + return AFMT_S16_LE; + } + + case AUDIO_FORMAT_U16: + if (endianness) { + return AFMT_U16_BE; + } else { + return AFMT_U16_LE; + } + + default: + dolog ("Internal logic error: Bad audio format %d\n", fmt); +#ifdef DEBUG_AUDIO + abort (); +#endif + return AFMT_U8; + } +} + +static int oss_to_audfmt (int ossfmt, AudioFormat *fmt, int *endianness) +{ + switch (ossfmt) { + case AFMT_S8: + *endianness = 0; + *fmt = AUDIO_FORMAT_S8; + break; + + case AFMT_U8: + *endianness = 0; + *fmt = AUDIO_FORMAT_U8; + break; + + case AFMT_S16_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_S16; + break; + + case AFMT_U16_LE: + *endianness = 0; + *fmt = AUDIO_FORMAT_U16; + break; + + case AFMT_S16_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_S16; + break; + + case AFMT_U16_BE: + *endianness = 1; + *fmt = AUDIO_FORMAT_U16; + break; + + default: + dolog ("Unrecognized audio format %d\n", ossfmt); + return -1; + } + + return 0; +} + +#if defined DEBUG_MISMATCHES || defined DEBUG +static void oss_dump_info (struct oss_params *req, struct oss_params *obt) +{ + dolog ("parameter | requested value | obtained value\n"); + dolog ("format | %10d | %10d\n", req->fmt, obt->fmt); + dolog ("channels | %10d | %10d\n", + req->nchannels, obt->nchannels); + dolog ("frequency | %10d | %10d\n", req->freq, obt->freq); + dolog ("nfrags | %10d | %10d\n", req->nfrags, obt->nfrags); + dolog ("fragsize | %10d | %10d\n", + req->fragsize, obt->fragsize); +} +#endif + +#ifdef USE_DSP_POLICY +static int oss_get_version (int fd, int *version, const char *typ) +{ + if (ioctl (fd, OSS_GETVERSION, &version)) { +#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + /* + * Looks like atm (20100109) FreeBSD knows OSS_GETVERSION + * since 7.x, but currently only on the mixer device (or in + * the Linuxolator), and in the native version that part of + * the code is in fact never reached so the ioctl fails anyway. + * Until this is fixed, just check the errno and if its what + * FreeBSD's sound drivers return atm assume they are new enough. + */ + if (errno == EINVAL) { + *version = 0x040000; + return 0; + } +#endif + oss_logerr2 (errno, typ, "Failed to get OSS version\n"); + return -1; + } + return 0; +} +#endif + +static int oss_open(int in, struct oss_params *req, audsettings *as, + struct oss_params *obt, int *pfd, Audiodev *dev) +{ + AudiodevOssOptions *oopts = &dev->u.oss; + AudiodevOssPerDirectionOptions *opdo = in ? oopts->in : oopts->out; + int fd; + int oflags = (oopts->has_exclusive && oopts->exclusive) ? O_EXCL : 0; + audio_buf_info abinfo; + int fmt, freq, nchannels; + int setfragment = 1; + const char *dspname = opdo->has_dev ? opdo->dev : "/dev/dsp"; + const char *typ = in ? "ADC" : "DAC"; +#ifdef USE_DSP_POLICY + int policy = oopts->has_dsp_policy ? oopts->dsp_policy : 5; +#endif + + /* Kludge needed to have working mmap on Linux */ + oflags |= (oopts->has_try_mmap && oopts->try_mmap) ? + O_RDWR : (in ? O_RDONLY : O_WRONLY); + + fd = open (dspname, oflags | O_NONBLOCK); + if (-1 == fd) { + oss_logerr2 (errno, typ, "Failed to open `%s'\n", dspname); + return -1; + } + + freq = req->freq; + nchannels = req->nchannels; + fmt = req->fmt; + req->nfrags = opdo->has_buffer_count ? opdo->buffer_count : 4; + req->fragsize = audio_buffer_bytes( + qapi_AudiodevOssPerDirectionOptions_base(opdo), as, 23220); + + if (ioctl (fd, SNDCTL_DSP_SAMPLESIZE, &fmt)) { + oss_logerr2 (errno, typ, "Failed to set sample size %d\n", req->fmt); + goto err; + } + + if (ioctl (fd, SNDCTL_DSP_CHANNELS, &nchannels)) { + oss_logerr2 (errno, typ, "Failed to set number of channels %d\n", + req->nchannels); + goto err; + } + + if (ioctl (fd, SNDCTL_DSP_SPEED, &freq)) { + oss_logerr2 (errno, typ, "Failed to set frequency %d\n", req->freq); + goto err; + } + + if (ioctl (fd, SNDCTL_DSP_NONBLOCK, NULL)) { + oss_logerr2 (errno, typ, "Failed to set non-blocking mode\n"); + goto err; + } + +#ifdef USE_DSP_POLICY + if (policy >= 0) { + int version; + + if (!oss_get_version (fd, &version, typ)) { + trace_oss_version(version); + + if (version >= 0x040000) { + int policy2 = policy; + if (ioctl(fd, SNDCTL_DSP_POLICY, &policy2)) { + oss_logerr2 (errno, typ, + "Failed to set timing policy to %d\n", + policy); + goto err; + } + setfragment = 0; + } + } + } +#endif + + if (setfragment) { + int mmmmssss = (req->nfrags << 16) | ctz32 (req->fragsize); + if (ioctl (fd, SNDCTL_DSP_SETFRAGMENT, &mmmmssss)) { + oss_logerr2 (errno, typ, "Failed to set buffer length (%d, %d)\n", + req->nfrags, req->fragsize); + goto err; + } + } + + if (ioctl (fd, in ? SNDCTL_DSP_GETISPACE : SNDCTL_DSP_GETOSPACE, &abinfo)) { + oss_logerr2 (errno, typ, "Failed to get buffer length\n"); + goto err; + } + + if (!abinfo.fragstotal || !abinfo.fragsize) { + AUD_log (AUDIO_CAP, "Returned bogus buffer information(%d, %d) for %s\n", + abinfo.fragstotal, abinfo.fragsize, typ); + goto err; + } + + obt->fmt = fmt; + obt->nchannels = nchannels; + obt->freq = freq; + obt->nfrags = abinfo.fragstotal; + obt->fragsize = abinfo.fragsize; + *pfd = fd; + +#ifdef DEBUG_MISMATCHES + if ((req->fmt != obt->fmt) || + (req->nchannels != obt->nchannels) || + (req->freq != obt->freq) || + (req->fragsize != obt->fragsize) || + (req->nfrags != obt->nfrags)) { + dolog ("Audio parameters mismatch\n"); + oss_dump_info (req, obt); + } +#endif + +#ifdef DEBUG + oss_dump_info (req, obt); +#endif + return 0; + + err: + oss_anal_close (&fd); + return -1; +} + +static size_t oss_get_available_bytes(OSSVoiceOut *oss) +{ + int err; + struct count_info cntinfo; + assert(oss->mmapped); + + err = ioctl(oss->fd, SNDCTL_DSP_GETOPTR, &cntinfo); + if (err < 0) { + oss_logerr(errno, "SNDCTL_DSP_GETOPTR failed\n"); + return 0; + } + + return audio_ring_dist(cntinfo.ptr, oss->hw.pos_emul, oss->hw.size_emul); +} + +static void oss_run_buffer_out(HWVoiceOut *hw) +{ + OSSVoiceOut *oss = (OSSVoiceOut *)hw; + + if (!oss->mmapped) { + audio_generic_run_buffer_out(hw); + } +} + +static void *oss_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + if (oss->mmapped) { + *size = MIN(oss_get_available_bytes(oss), hw->size_emul - hw->pos_emul); + return hw->buf_emul + hw->pos_emul; + } else { + return audio_generic_get_buffer_out(hw, size); + } +} + +static size_t oss_put_buffer_out(HWVoiceOut *hw, void *buf, size_t size) +{ + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + if (oss->mmapped) { + assert(buf == hw->buf_emul + hw->pos_emul && size < hw->size_emul); + + hw->pos_emul = (hw->pos_emul + size) % hw->size_emul; + return size; + } else { + return audio_generic_put_buffer_out(hw, buf, size); + } +} + +static size_t oss_write(HWVoiceOut *hw, void *buf, size_t len) +{ + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + size_t pos; + + if (oss->mmapped) { + size_t total_len; + len = MIN(len, oss_get_available_bytes(oss)); + + total_len = len; + while (len) { + size_t to_copy = MIN(len, hw->size_emul - hw->pos_emul); + memcpy(hw->buf_emul + hw->pos_emul, buf, to_copy); + + hw->pos_emul = (hw->pos_emul + to_copy) % hw->size_emul; + buf += to_copy; + len -= to_copy; + } + return total_len; + } + + pos = 0; + while (len) { + ssize_t bytes_written; + void *pcm = advance(buf, pos); + + bytes_written = write(oss->fd, pcm, len); + if (bytes_written < 0) { + if (errno != EAGAIN) { + oss_logerr(errno, "failed to write %zu bytes\n", + len); + } + return pos; + } + + pos += bytes_written; + if (bytes_written < len) { + break; + } + len -= bytes_written; + } + return pos; +} + +static void oss_fini_out (HWVoiceOut *hw) +{ + int err; + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + + ldebug ("oss_fini\n"); + oss_anal_close (&oss->fd); + + if (oss->mmapped && hw->buf_emul) { + err = munmap(hw->buf_emul, hw->size_emul); + if (err) { + oss_logerr(errno, "Failed to unmap buffer %p, size %zu\n", + hw->buf_emul, hw->size_emul); + } + hw->buf_emul = NULL; + } +} + +static int oss_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + struct oss_params req, obt; + int endianness; + int err; + int fd; + AudioFormat effective_fmt; + struct audsettings obt_as; + Audiodev *dev = drv_opaque; + AudiodevOssOptions *oopts = &dev->u.oss; + + oss->fd = -1; + + req.fmt = aud_to_ossfmt (as->fmt, as->endianness); + req.freq = as->freq; + req.nchannels = as->nchannels; + + if (oss_open(0, &req, as, &obt, &fd, dev)) { + return -1; + } + + err = oss_to_audfmt (obt.fmt, &effective_fmt, &endianness); + if (err) { + oss_anal_close (&fd); + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.nchannels; + obt_as.fmt = effective_fmt; + obt_as.endianness = endianness; + + audio_pcm_init_info (&hw->info, &obt_as); + oss->nfrags = obt.nfrags; + oss->fragsize = obt.fragsize; + + if (obt.nfrags * obt.fragsize % hw->info.bytes_per_frame) { + dolog ("warning: Misaligned DAC buffer, size %d, alignment %d\n", + obt.nfrags * obt.fragsize, hw->info.bytes_per_frame); + } + + hw->samples = (obt.nfrags * obt.fragsize) / hw->info.bytes_per_frame; + + oss->mmapped = 0; + if (oopts->has_try_mmap && oopts->try_mmap) { + hw->size_emul = hw->samples * hw->info.bytes_per_frame; + hw->buf_emul = mmap( + NULL, + hw->size_emul, + PROT_READ | PROT_WRITE, + MAP_SHARED, + fd, + 0 + ); + if (hw->buf_emul == MAP_FAILED) { + oss_logerr(errno, "Failed to map %zu bytes of DAC\n", + hw->size_emul); + hw->buf_emul = NULL; + } else { + int err; + int trig = 0; + if (ioctl (fd, SNDCTL_DSP_SETTRIGGER, &trig) < 0) { + oss_logerr (errno, "SNDCTL_DSP_SETTRIGGER 0 failed\n"); + } else { + trig = PCM_ENABLE_OUTPUT; + if (ioctl (fd, SNDCTL_DSP_SETTRIGGER, &trig) < 0) { + oss_logerr ( + errno, + "SNDCTL_DSP_SETTRIGGER PCM_ENABLE_OUTPUT failed\n" + ); + } else { + oss->mmapped = 1; + } + } + + if (!oss->mmapped) { + err = munmap(hw->buf_emul, hw->size_emul); + if (err) { + oss_logerr(errno, "Failed to unmap buffer %p size %zu\n", + hw->buf_emul, hw->size_emul); + } + hw->buf_emul = NULL; + } + } + } + + oss->fd = fd; + oss->dev = dev; + return 0; +} + +static void oss_enable_out(HWVoiceOut *hw, bool enable) +{ + int trig; + OSSVoiceOut *oss = (OSSVoiceOut *) hw; + AudiodevOssPerDirectionOptions *opdo = oss->dev->u.oss.out; + + if (enable) { + hw->poll_mode = opdo->try_poll; + + ldebug("enabling voice\n"); + if (hw->poll_mode) { + oss_poll_out(hw); + } + + if (!oss->mmapped) { + return; + } + + audio_pcm_info_clear_buf(&hw->info, hw->buf_emul, hw->samples); + trig = PCM_ENABLE_OUTPUT; + if (ioctl(oss->fd, SNDCTL_DSP_SETTRIGGER, &trig) < 0) { + oss_logerr(errno, + "SNDCTL_DSP_SETTRIGGER PCM_ENABLE_OUTPUT failed\n"); + return; + } + } else { + if (hw->poll_mode) { + qemu_set_fd_handler (oss->fd, NULL, NULL, NULL); + hw->poll_mode = 0; + } + + if (!oss->mmapped) { + return; + } + + ldebug ("disabling voice\n"); + trig = 0; + if (ioctl (oss->fd, SNDCTL_DSP_SETTRIGGER, &trig) < 0) { + oss_logerr (errno, "SNDCTL_DSP_SETTRIGGER 0 failed\n"); + return; + } + } +} + +static int oss_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + OSSVoiceIn *oss = (OSSVoiceIn *) hw; + struct oss_params req, obt; + int endianness; + int err; + int fd; + AudioFormat effective_fmt; + struct audsettings obt_as; + Audiodev *dev = drv_opaque; + + oss->fd = -1; + + req.fmt = aud_to_ossfmt (as->fmt, as->endianness); + req.freq = as->freq; + req.nchannels = as->nchannels; + if (oss_open(1, &req, as, &obt, &fd, dev)) { + return -1; + } + + err = oss_to_audfmt (obt.fmt, &effective_fmt, &endianness); + if (err) { + oss_anal_close (&fd); + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.nchannels; + obt_as.fmt = effective_fmt; + obt_as.endianness = endianness; + + audio_pcm_init_info (&hw->info, &obt_as); + oss->nfrags = obt.nfrags; + oss->fragsize = obt.fragsize; + + if (obt.nfrags * obt.fragsize % hw->info.bytes_per_frame) { + dolog ("warning: Misaligned ADC buffer, size %d, alignment %d\n", + obt.nfrags * obt.fragsize, hw->info.bytes_per_frame); + } + + hw->samples = (obt.nfrags * obt.fragsize) / hw->info.bytes_per_frame; + + oss->fd = fd; + oss->dev = dev; + return 0; +} + +static void oss_fini_in (HWVoiceIn *hw) +{ + OSSVoiceIn *oss = (OSSVoiceIn *) hw; + + oss_anal_close (&oss->fd); +} + +static size_t oss_read(HWVoiceIn *hw, void *buf, size_t len) +{ + OSSVoiceIn *oss = (OSSVoiceIn *) hw; + size_t pos = 0; + + while (len) { + ssize_t nread; + + void *dst = advance(buf, pos); + nread = read(oss->fd, dst, len); + + if (nread == -1) { + switch (errno) { + case EINTR: + case EAGAIN: + break; + default: + oss_logerr(errno, "Failed to read %zu bytes of audio (to %p)\n", + len, dst); + break; + } + break; + } + + pos += nread; + len -= nread; + } + + return pos; +} + +static void oss_enable_in(HWVoiceIn *hw, bool enable) +{ + OSSVoiceIn *oss = (OSSVoiceIn *) hw; + AudiodevOssPerDirectionOptions *opdo = oss->dev->u.oss.out; + + if (enable) { + hw->poll_mode = opdo->try_poll; + + if (hw->poll_mode) { + oss_poll_in(hw); + } + } else { + if (hw->poll_mode) { + qemu_set_fd_handler (oss->fd, NULL, NULL, NULL); + hw->poll_mode = 0; + } + } +} + +static void oss_init_per_direction(AudiodevOssPerDirectionOptions *opdo) +{ + if (!opdo->has_try_poll) { + opdo->try_poll = true; + opdo->has_try_poll = true; + } +} + +static void *oss_audio_init(Audiodev *dev) +{ + AudiodevOssOptions *oopts; + assert(dev->driver == AUDIODEV_DRIVER_OSS); + + oopts = &dev->u.oss; + oss_init_per_direction(oopts->in); + oss_init_per_direction(oopts->out); + + if (access(oopts->in->has_dev ? oopts->in->dev : "/dev/dsp", + R_OK | W_OK) < 0 || + access(oopts->out->has_dev ? oopts->out->dev : "/dev/dsp", + R_OK | W_OK) < 0) { + return NULL; + } + return dev; +} + +static void oss_audio_fini (void *opaque) +{ +} + +static struct audio_pcm_ops oss_pcm_ops = { + .init_out = oss_init_out, + .fini_out = oss_fini_out, + .write = oss_write, + .run_buffer_out = oss_run_buffer_out, + .get_buffer_out = oss_get_buffer_out, + .put_buffer_out = oss_put_buffer_out, + .enable_out = oss_enable_out, + + .init_in = oss_init_in, + .fini_in = oss_fini_in, + .read = oss_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = oss_enable_in +}; + +static struct audio_driver oss_audio_driver = { + .name = "oss", + .descr = "OSS http://www.opensound.com", + .init = oss_audio_init, + .fini = oss_audio_fini, + .pcm_ops = &oss_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof (OSSVoiceOut), + .voice_size_in = sizeof (OSSVoiceIn) +}; + +static void register_audio_oss(void) +{ + audio_driver_register(&oss_audio_driver); +} +type_init(register_audio_oss); diff --git a/audio/paaudio.c b/audio/paaudio.c new file mode 100644 index 000000000..75401d539 --- /dev/null +++ b/audio/paaudio.c @@ -0,0 +1,933 @@ +/* public domain */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "audio.h" +#include "qapi/opts-visitor.h" + +#include <pulse/pulseaudio.h> + +#define AUDIO_CAP "pulseaudio" +#include "audio_int.h" + +typedef struct PAConnection { + char *server; + int refcount; + QTAILQ_ENTRY(PAConnection) list; + + pa_threaded_mainloop *mainloop; + pa_context *context; +} PAConnection; + +static QTAILQ_HEAD(PAConnectionHead, PAConnection) pa_conns = + QTAILQ_HEAD_INITIALIZER(pa_conns); + +typedef struct { + Audiodev *dev; + PAConnection *conn; +} paaudio; + +typedef struct { + HWVoiceOut hw; + pa_stream *stream; + paaudio *g; +} PAVoiceOut; + +typedef struct { + HWVoiceIn hw; + pa_stream *stream; + const void *read_data; + size_t read_length; + paaudio *g; +} PAVoiceIn; + +static void qpa_conn_fini(PAConnection *c); + +static void GCC_FMT_ATTR (2, 3) qpa_logerr (int err, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", pa_strerror (err)); +} + +#ifndef PA_CONTEXT_IS_GOOD +static inline int PA_CONTEXT_IS_GOOD(pa_context_state_t x) +{ + return + x == PA_CONTEXT_CONNECTING || + x == PA_CONTEXT_AUTHORIZING || + x == PA_CONTEXT_SETTING_NAME || + x == PA_CONTEXT_READY; +} +#endif + +#ifndef PA_STREAM_IS_GOOD +static inline int PA_STREAM_IS_GOOD(pa_stream_state_t x) +{ + return + x == PA_STREAM_CREATING || + x == PA_STREAM_READY; +} +#endif + +#define CHECK_SUCCESS_GOTO(c, expression, label, msg) \ + do { \ + if (!(expression)) { \ + qpa_logerr(pa_context_errno((c)->context), msg); \ + goto label; \ + } \ + } while (0) + +#define CHECK_DEAD_GOTO(c, stream, label, msg) \ + do { \ + if (!(c)->context || !PA_CONTEXT_IS_GOOD (pa_context_get_state((c)->context)) || \ + !(stream) || !PA_STREAM_IS_GOOD (pa_stream_get_state ((stream)))) { \ + if (((c)->context && pa_context_get_state ((c)->context) == PA_CONTEXT_FAILED) || \ + ((stream) && pa_stream_get_state ((stream)) == PA_STREAM_FAILED)) { \ + qpa_logerr(pa_context_errno((c)->context), msg); \ + } else { \ + qpa_logerr(PA_ERR_BADSTATE, msg); \ + } \ + goto label; \ + } \ + } while (0) + +static void *qpa_get_buffer_in(HWVoiceIn *hw, size_t *size) +{ + PAVoiceIn *p = (PAVoiceIn *) hw; + PAConnection *c = p->g->conn; + int r; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail, + "pa_threaded_mainloop_lock failed\n"); + + if (!p->read_length) { + r = pa_stream_peek(p->stream, &p->read_data, &p->read_length); + CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail, + "pa_stream_peek failed\n"); + } + + *size = MIN(p->read_length, *size); + + pa_threaded_mainloop_unlock(c->mainloop); + return (void *) p->read_data; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); + *size = 0; + return NULL; +} + +static void qpa_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size) +{ + PAVoiceIn *p = (PAVoiceIn *) hw; + PAConnection *c = p->g->conn; + int r; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock, + "pa_threaded_mainloop_lock failed\n"); + + assert(buf == p->read_data && size <= p->read_length); + + p->read_data += size; + p->read_length -= size; + + if (size && !p->read_length) { + r = pa_stream_drop(p->stream); + CHECK_SUCCESS_GOTO(c, r == 0, unlock, "pa_stream_drop failed\n"); + } + +unlock: + pa_threaded_mainloop_unlock(c->mainloop); +} + +static size_t qpa_read(HWVoiceIn *hw, void *data, size_t length) +{ + PAVoiceIn *p = (PAVoiceIn *) hw; + PAConnection *c = p->g->conn; + size_t total = 0; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail, + "pa_threaded_mainloop_lock failed\n"); + if (pa_stream_get_state(p->stream) != PA_STREAM_READY) { + /* wait for stream to become ready */ + goto unlock; + } + + while (total < length) { + size_t l; + int r; + + if (!p->read_length) { + r = pa_stream_peek(p->stream, &p->read_data, &p->read_length); + CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail, + "pa_stream_peek failed\n"); + if (!p->read_length) { + /* buffer is empty */ + break; + } + } + + l = MIN(p->read_length, length - total); + memcpy((char *)data + total, p->read_data, l); + + p->read_data += l; + p->read_length -= l; + total += l; + + if (!p->read_length) { + r = pa_stream_drop(p->stream); + CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail, + "pa_stream_drop failed\n"); + } + } + +unlock: + pa_threaded_mainloop_unlock(c->mainloop); + return total; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); + return 0; +} + +static void *qpa_get_buffer_out(HWVoiceOut *hw, size_t *size) +{ + PAVoiceOut *p = (PAVoiceOut *) hw; + PAConnection *c = p->g->conn; + void *ret; + size_t l; + int r; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail, + "pa_threaded_mainloop_lock failed\n"); + if (pa_stream_get_state(p->stream) != PA_STREAM_READY) { + /* wait for stream to become ready */ + l = 0; + ret = NULL; + goto unlock; + } + + l = pa_stream_writable_size(p->stream); + CHECK_SUCCESS_GOTO(c, l != (size_t) -1, unlock_and_fail, + "pa_stream_writable_size failed\n"); + + *size = -1; + r = pa_stream_begin_write(p->stream, &ret, size); + CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail, + "pa_stream_begin_write failed\n"); + +unlock: + pa_threaded_mainloop_unlock(c->mainloop); + if (*size > l) { + *size = l; + } + return ret; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); + *size = 0; + return NULL; +} + +static size_t qpa_put_buffer_out(HWVoiceOut *hw, void *data, size_t length) +{ + PAVoiceOut *p = (PAVoiceOut *)hw; + PAConnection *c = p->g->conn; + int r; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail, + "pa_threaded_mainloop_lock failed\n"); + + r = pa_stream_write(p->stream, data, length, NULL, 0LL, PA_SEEK_RELATIVE); + CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail, "pa_stream_write failed\n"); + + pa_threaded_mainloop_unlock(c->mainloop); + return length; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); + return 0; +} + +static size_t qpa_write(HWVoiceOut *hw, void *data, size_t length) +{ + PAVoiceOut *p = (PAVoiceOut *) hw; + PAConnection *c = p->g->conn; + size_t l; + int r; + + pa_threaded_mainloop_lock(c->mainloop); + + CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail, + "pa_threaded_mainloop_lock failed\n"); + if (pa_stream_get_state(p->stream) != PA_STREAM_READY) { + /* wait for stream to become ready */ + l = 0; + goto unlock; + } + + l = pa_stream_writable_size(p->stream); + + CHECK_SUCCESS_GOTO(c, l != (size_t) -1, unlock_and_fail, + "pa_stream_writable_size failed\n"); + + if (l > length) { + l = length; + } + + r = pa_stream_write(p->stream, data, l, NULL, 0LL, PA_SEEK_RELATIVE); + CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail, "pa_stream_write failed\n"); + +unlock: + pa_threaded_mainloop_unlock(c->mainloop); + return l; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); + return 0; +} + +static pa_sample_format_t audfmt_to_pa (AudioFormat afmt, int endianness) +{ + int format; + + switch (afmt) { + case AUDIO_FORMAT_S8: + case AUDIO_FORMAT_U8: + format = PA_SAMPLE_U8; + break; + case AUDIO_FORMAT_S16: + case AUDIO_FORMAT_U16: + format = endianness ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE; + break; + case AUDIO_FORMAT_S32: + case AUDIO_FORMAT_U32: + format = endianness ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE; + break; + case AUDIO_FORMAT_F32: + format = endianness ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE; + break; + default: + dolog ("Internal logic error: Bad audio format %d\n", afmt); + format = PA_SAMPLE_U8; + break; + } + return format; +} + +static AudioFormat pa_to_audfmt (pa_sample_format_t fmt, int *endianness) +{ + switch (fmt) { + case PA_SAMPLE_U8: + return AUDIO_FORMAT_U8; + case PA_SAMPLE_S16BE: + *endianness = 1; + return AUDIO_FORMAT_S16; + case PA_SAMPLE_S16LE: + *endianness = 0; + return AUDIO_FORMAT_S16; + case PA_SAMPLE_S32BE: + *endianness = 1; + return AUDIO_FORMAT_S32; + case PA_SAMPLE_S32LE: + *endianness = 0; + return AUDIO_FORMAT_S32; + case PA_SAMPLE_FLOAT32BE: + *endianness = 1; + return AUDIO_FORMAT_F32; + case PA_SAMPLE_FLOAT32LE: + *endianness = 0; + return AUDIO_FORMAT_F32; + default: + dolog ("Internal logic error: Bad pa_sample_format %d\n", fmt); + return AUDIO_FORMAT_U8; + } +} + +static void context_state_cb (pa_context *c, void *userdata) +{ + PAConnection *conn = userdata; + + switch (pa_context_get_state(c)) { + case PA_CONTEXT_READY: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + pa_threaded_mainloop_signal(conn->mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } +} + +static void stream_state_cb (pa_stream *s, void * userdata) +{ + PAConnection *c = userdata; + + switch (pa_stream_get_state (s)) { + + case PA_STREAM_READY: + case PA_STREAM_FAILED: + case PA_STREAM_TERMINATED: + pa_threaded_mainloop_signal(c->mainloop, 0); + break; + + case PA_STREAM_UNCONNECTED: + case PA_STREAM_CREATING: + break; + } +} + +static pa_stream *qpa_simple_new ( + PAConnection *c, + const char *name, + pa_stream_direction_t dir, + const char *dev, + const pa_sample_spec *ss, + const pa_buffer_attr *attr, + int *rerror) +{ + int r; + pa_stream *stream = NULL; + pa_stream_flags_t flags; + pa_channel_map map; + + pa_threaded_mainloop_lock(c->mainloop); + + pa_channel_map_init(&map); + map.channels = ss->channels; + + /* + * TODO: This currently expects the only frontend supporting more than 2 + * channels is the usb-audio. We will need some means to set channel + * order when a new frontend gains multi-channel support. + */ + switch (ss->channels) { + case 1: + map.map[0] = PA_CHANNEL_POSITION_MONO; + break; + + case 2: + map.map[0] = PA_CHANNEL_POSITION_LEFT; + map.map[1] = PA_CHANNEL_POSITION_RIGHT; + break; + + case 6: + map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT; + map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT; + map.map[2] = PA_CHANNEL_POSITION_CENTER; + map.map[3] = PA_CHANNEL_POSITION_LFE; + map.map[4] = PA_CHANNEL_POSITION_REAR_LEFT; + map.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT; + break; + + case 8: + map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT; + map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT; + map.map[2] = PA_CHANNEL_POSITION_CENTER; + map.map[3] = PA_CHANNEL_POSITION_LFE; + map.map[4] = PA_CHANNEL_POSITION_REAR_LEFT; + map.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT; + map.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT; + map.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT; + break; + + default: + dolog("Internal error: unsupported channel count %d\n", ss->channels); + goto fail; + } + + stream = pa_stream_new(c->context, name, ss, &map); + if (!stream) { + goto fail; + } + + pa_stream_set_state_callback(stream, stream_state_cb, c); + + flags = PA_STREAM_EARLY_REQUESTS; + + if (dev) { + /* don't move the stream if the user specified a sink/source */ + flags |= PA_STREAM_DONT_MOVE; + } + + if (dir == PA_STREAM_PLAYBACK) { + r = pa_stream_connect_playback(stream, dev, attr, flags, NULL, NULL); + } else { + r = pa_stream_connect_record(stream, dev, attr, flags); + } + + if (r < 0) { + goto fail; + } + + pa_threaded_mainloop_unlock(c->mainloop); + + return stream; + +fail: + pa_threaded_mainloop_unlock(c->mainloop); + + if (stream) { + pa_stream_unref (stream); + } + + *rerror = pa_context_errno(c->context); + + return NULL; +} + +static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + int error; + pa_sample_spec ss; + pa_buffer_attr ba; + struct audsettings obt_as = *as; + PAVoiceOut *pa = (PAVoiceOut *) hw; + paaudio *g = pa->g = drv_opaque; + AudiodevPaOptions *popts = &g->dev->u.pa; + AudiodevPaPerDirectionOptions *ppdo = popts->out; + PAConnection *c = g->conn; + + ss.format = audfmt_to_pa (as->fmt, as->endianness); + ss.channels = as->nchannels; + ss.rate = as->freq; + + ba.tlength = pa_usec_to_bytes(ppdo->latency, &ss); + ba.minreq = pa_usec_to_bytes(MIN(ppdo->latency >> 2, + (g->dev->timer_period >> 2) * 3), &ss); + ba.maxlength = -1; + ba.prebuf = -1; + + obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness); + + pa->stream = qpa_simple_new ( + c, + ppdo->has_stream_name ? ppdo->stream_name : g->dev->id, + PA_STREAM_PLAYBACK, + ppdo->has_name ? ppdo->name : NULL, + &ss, + &ba, /* buffering attributes */ + &error + ); + if (!pa->stream) { + qpa_logerr (error, "pa_simple_new for playback failed\n"); + goto fail1; + } + + audio_pcm_init_info (&hw->info, &obt_as); + /* + * This is wrong. hw->samples counts in frames. hw->samples will be + * number of channels times larger than expected. + */ + hw->samples = audio_buffer_samples( + qapi_AudiodevPaPerDirectionOptions_base(ppdo), &obt_as, 46440); + + return 0; + + fail1: + return -1; +} + +static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + int error; + pa_sample_spec ss; + pa_buffer_attr ba; + struct audsettings obt_as = *as; + PAVoiceIn *pa = (PAVoiceIn *) hw; + paaudio *g = pa->g = drv_opaque; + AudiodevPaOptions *popts = &g->dev->u.pa; + AudiodevPaPerDirectionOptions *ppdo = popts->in; + PAConnection *c = g->conn; + + ss.format = audfmt_to_pa (as->fmt, as->endianness); + ss.channels = as->nchannels; + ss.rate = as->freq; + + ba.fragsize = pa_usec_to_bytes((g->dev->timer_period >> 1) * 3, &ss); + ba.maxlength = pa_usec_to_bytes( + MAX(ppdo->latency, g->dev->timer_period * 3), &ss); + ba.minreq = -1; + ba.prebuf = -1; + + obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness); + + pa->stream = qpa_simple_new ( + c, + ppdo->has_stream_name ? ppdo->stream_name : g->dev->id, + PA_STREAM_RECORD, + ppdo->has_name ? ppdo->name : NULL, + &ss, + &ba, /* buffering attributes */ + &error + ); + if (!pa->stream) { + qpa_logerr (error, "pa_simple_new for capture failed\n"); + goto fail1; + } + + audio_pcm_init_info (&hw->info, &obt_as); + /* + * This is wrong. hw->samples counts in frames. hw->samples will be + * number of channels times larger than expected. + */ + hw->samples = audio_buffer_samples( + qapi_AudiodevPaPerDirectionOptions_base(ppdo), &obt_as, 46440); + + return 0; + + fail1: + return -1; +} + +static void qpa_simple_disconnect(PAConnection *c, pa_stream *stream) +{ + int err; + + /* + * wait until actually connects. workaround pa bug #247 + * https://gitlab.freedesktop.org/pulseaudio/pulseaudio/issues/247 + */ + while (pa_stream_get_state(stream) == PA_STREAM_CREATING) { + pa_threaded_mainloop_wait(c->mainloop); + } + + err = pa_stream_disconnect(stream); + if (err != 0) { + dolog("Failed to disconnect! err=%d\n", err); + } + pa_stream_unref(stream); +} + +static void qpa_fini_out (HWVoiceOut *hw) +{ + PAVoiceOut *pa = (PAVoiceOut *) hw; + + if (pa->stream) { + PAConnection *c = pa->g->conn; + + pa_threaded_mainloop_lock(c->mainloop); + qpa_simple_disconnect(c, pa->stream); + pa->stream = NULL; + pa_threaded_mainloop_unlock(c->mainloop); + } +} + +static void qpa_fini_in (HWVoiceIn *hw) +{ + PAVoiceIn *pa = (PAVoiceIn *) hw; + + if (pa->stream) { + PAConnection *c = pa->g->conn; + + pa_threaded_mainloop_lock(c->mainloop); + if (pa->read_length) { + int r = pa_stream_drop(pa->stream); + if (r) { + qpa_logerr(pa_context_errno(c->context), + "pa_stream_drop failed\n"); + } + pa->read_length = 0; + } + qpa_simple_disconnect(c, pa->stream); + pa->stream = NULL; + pa_threaded_mainloop_unlock(c->mainloop); + } +} + +static void qpa_volume_out(HWVoiceOut *hw, Volume *vol) +{ + PAVoiceOut *pa = (PAVoiceOut *) hw; + pa_operation *op; + pa_cvolume v; + PAConnection *c = pa->g->conn; + int i; + +#ifdef PA_CHECK_VERSION /* macro is present in 0.9.16+ */ + pa_cvolume_init (&v); /* function is present in 0.9.13+ */ +#endif + + v.channels = vol->channels; + for (i = 0; i < vol->channels; ++i) { + v.values[i] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * vol->vol[i]) / 255; + } + + pa_threaded_mainloop_lock(c->mainloop); + + op = pa_context_set_sink_input_volume(c->context, + pa_stream_get_index(pa->stream), + &v, NULL, NULL); + if (!op) { + qpa_logerr(pa_context_errno(c->context), + "set_sink_input_volume() failed\n"); + } else { + pa_operation_unref(op); + } + + op = pa_context_set_sink_input_mute(c->context, + pa_stream_get_index(pa->stream), + vol->mute, NULL, NULL); + if (!op) { + qpa_logerr(pa_context_errno(c->context), + "set_sink_input_mute() failed\n"); + } else { + pa_operation_unref(op); + } + + pa_threaded_mainloop_unlock(c->mainloop); +} + +static void qpa_volume_in(HWVoiceIn *hw, Volume *vol) +{ + PAVoiceIn *pa = (PAVoiceIn *) hw; + pa_operation *op; + pa_cvolume v; + PAConnection *c = pa->g->conn; + int i; + +#ifdef PA_CHECK_VERSION + pa_cvolume_init (&v); +#endif + + v.channels = vol->channels; + for (i = 0; i < vol->channels; ++i) { + v.values[i] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * vol->vol[i]) / 255; + } + + pa_threaded_mainloop_lock(c->mainloop); + + op = pa_context_set_source_output_volume(c->context, + pa_stream_get_index(pa->stream), + &v, NULL, NULL); + if (!op) { + qpa_logerr(pa_context_errno(c->context), + "set_source_output_volume() failed\n"); + } else { + pa_operation_unref(op); + } + + op = pa_context_set_source_output_mute(c->context, + pa_stream_get_index(pa->stream), + vol->mute, NULL, NULL); + if (!op) { + qpa_logerr(pa_context_errno(c->context), + "set_source_output_mute() failed\n"); + } else { + pa_operation_unref(op); + } + + pa_threaded_mainloop_unlock(c->mainloop); +} + +static int qpa_validate_per_direction_opts(Audiodev *dev, + AudiodevPaPerDirectionOptions *pdo) +{ + if (!pdo->has_latency) { + pdo->has_latency = true; + pdo->latency = 15000; + } + return 1; +} + +/* common */ +static void *qpa_conn_init(const char *server) +{ + PAConnection *c = g_malloc0(sizeof(PAConnection)); + QTAILQ_INSERT_TAIL(&pa_conns, c, list); + + c->mainloop = pa_threaded_mainloop_new(); + if (!c->mainloop) { + goto fail; + } + + c->context = pa_context_new(pa_threaded_mainloop_get_api(c->mainloop), + audio_application_name()); + if (!c->context) { + goto fail; + } + + pa_context_set_state_callback(c->context, context_state_cb, c); + + if (pa_context_connect(c->context, server, 0, NULL) < 0) { + qpa_logerr(pa_context_errno(c->context), + "pa_context_connect() failed\n"); + goto fail; + } + + pa_threaded_mainloop_lock(c->mainloop); + + if (pa_threaded_mainloop_start(c->mainloop) < 0) { + goto unlock_and_fail; + } + + for (;;) { + pa_context_state_t state; + + state = pa_context_get_state(c->context); + + if (state == PA_CONTEXT_READY) { + break; + } + + if (!PA_CONTEXT_IS_GOOD(state)) { + qpa_logerr(pa_context_errno(c->context), + "Wrong context state\n"); + goto unlock_and_fail; + } + + /* Wait until the context is ready */ + pa_threaded_mainloop_wait(c->mainloop); + } + + pa_threaded_mainloop_unlock(c->mainloop); + return c; + +unlock_and_fail: + pa_threaded_mainloop_unlock(c->mainloop); +fail: + AUD_log (AUDIO_CAP, "Failed to initialize PA context"); + qpa_conn_fini(c); + return NULL; +} + +static void *qpa_audio_init(Audiodev *dev) +{ + paaudio *g; + AudiodevPaOptions *popts = &dev->u.pa; + const char *server; + PAConnection *c; + + assert(dev->driver == AUDIODEV_DRIVER_PA); + + if (!popts->has_server) { + char pidfile[64]; + char *runtime; + struct stat st; + + runtime = getenv("XDG_RUNTIME_DIR"); + if (!runtime) { + return NULL; + } + snprintf(pidfile, sizeof(pidfile), "%s/pulse/pid", runtime); + if (stat(pidfile, &st) != 0) { + return NULL; + } + } + + if (!qpa_validate_per_direction_opts(dev, popts->in)) { + return NULL; + } + if (!qpa_validate_per_direction_opts(dev, popts->out)) { + return NULL; + } + + g = g_malloc0(sizeof(paaudio)); + server = popts->has_server ? popts->server : NULL; + + g->dev = dev; + + QTAILQ_FOREACH(c, &pa_conns, list) { + if (server == NULL || c->server == NULL ? + server == c->server : + strcmp(server, c->server) == 0) { + g->conn = c; + break; + } + } + if (!g->conn) { + g->conn = qpa_conn_init(server); + } + if (!g->conn) { + g_free(g); + return NULL; + } + + ++g->conn->refcount; + return g; +} + +static void qpa_conn_fini(PAConnection *c) +{ + if (c->mainloop) { + pa_threaded_mainloop_stop(c->mainloop); + } + + if (c->context) { + pa_context_disconnect(c->context); + pa_context_unref(c->context); + } + + if (c->mainloop) { + pa_threaded_mainloop_free(c->mainloop); + } + + QTAILQ_REMOVE(&pa_conns, c, list); + g_free(c); +} + +static void qpa_audio_fini (void *opaque) +{ + paaudio *g = opaque; + PAConnection *c = g->conn; + + if (--c->refcount == 0) { + qpa_conn_fini(c); + } + + g_free(g); +} + +static struct audio_pcm_ops qpa_pcm_ops = { + .init_out = qpa_init_out, + .fini_out = qpa_fini_out, + .write = qpa_write, + .get_buffer_out = qpa_get_buffer_out, + .put_buffer_out = qpa_put_buffer_out, + .volume_out = qpa_volume_out, + + .init_in = qpa_init_in, + .fini_in = qpa_fini_in, + .read = qpa_read, + .get_buffer_in = qpa_get_buffer_in, + .put_buffer_in = qpa_put_buffer_in, + .volume_in = qpa_volume_in +}; + +static struct audio_driver pa_audio_driver = { + .name = "pa", + .descr = "http://www.pulseaudio.org/", + .init = qpa_audio_init, + .fini = qpa_audio_fini, + .pcm_ops = &qpa_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof (PAVoiceOut), + .voice_size_in = sizeof (PAVoiceIn), +}; + +static void register_audio_pa(void) +{ + audio_driver_register(&pa_audio_driver); +} +type_init(register_audio_pa); diff --git a/audio/rate_template.h b/audio/rate_template.h new file mode 100644 index 000000000..f94c940c6 --- /dev/null +++ b/audio/rate_template.h @@ -0,0 +1,117 @@ +/* + * QEMU Mixing engine + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * Copyright (c) 1998 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * Processed signed long samples from ibuf to obuf. + * Return number of samples processed. + */ +void NAME (void *opaque, struct st_sample *ibuf, struct st_sample *obuf, + size_t *isamp, size_t *osamp) +{ + struct rate *rate = opaque; + struct st_sample *istart, *iend; + struct st_sample *ostart, *oend; + struct st_sample ilast, icur, out; +#ifdef FLOAT_MIXENG + mixeng_real t; +#else + int64_t t; +#endif + + ilast = rate->ilast; + + istart = ibuf; + iend = ibuf + *isamp; + + ostart = obuf; + oend = obuf + *osamp; + + if (rate->opos_inc == (1ULL + UINT_MAX)) { + int i, n = *isamp > *osamp ? *osamp : *isamp; + for (i = 0; i < n; i++) { + OP (obuf[i].l, ibuf[i].l); + OP (obuf[i].r, ibuf[i].r); + } + *isamp = n; + *osamp = n; + return; + } + + while (obuf < oend) { + + /* Safety catch to make sure we have input samples. */ + if (ibuf >= iend) { + break; + } + + /* read as many input samples so that ipos > opos */ + + while (rate->ipos <= (rate->opos >> 32)) { + ilast = *ibuf++; + rate->ipos++; + + /* if ipos overflow, there is a infinite loop */ + if (rate->ipos == 0xffffffff) { + rate->ipos = 1; + rate->opos = rate->opos & 0xffffffff; + } + /* See if we finished the input buffer yet */ + if (ibuf >= iend) { + goto the_end; + } + } + + icur = *ibuf; + + /* interpolate */ +#ifdef FLOAT_MIXENG +#ifdef RECIPROCAL + t = (rate->opos & UINT_MAX) * (1.f / UINT_MAX); +#else + t = (rate->opos & UINT_MAX) / (mixeng_real) UINT_MAX; +#endif + out.l = (ilast.l * (1.0 - t)) + icur.l * t; + out.r = (ilast.r * (1.0 - t)) + icur.r * t; +#else + t = rate->opos & 0xffffffff; + out.l = (ilast.l * ((int64_t) UINT_MAX - t) + icur.l * t) >> 32; + out.r = (ilast.r * ((int64_t) UINT_MAX - t) + icur.r * t) >> 32; +#endif + + /* output sample & increment position */ + OP (obuf->l, out.l); + OP (obuf->r, out.r); + obuf += 1; + rate->opos += rate->opos_inc; + } + +the_end: + *isamp = ibuf - istart; + *osamp = obuf - ostart; + rate->ilast = ilast; +} + +#undef NAME +#undef OP diff --git a/audio/sdlaudio.c b/audio/sdlaudio.c new file mode 100644 index 000000000..c68c62a3e --- /dev/null +++ b/audio/sdlaudio.c @@ -0,0 +1,508 @@ +/* + * QEMU SDL audio driver + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include <SDL.h> +#include <SDL_thread.h> +#include "qemu/module.h" +#include "audio.h" + +#ifndef _WIN32 +#ifdef __sun__ +#define _POSIX_PTHREAD_SEMANTICS 1 +#elif defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) +#include <pthread.h> +#endif +#endif + +#define AUDIO_CAP "sdl" +#include "audio_int.h" + +typedef struct SDLVoiceOut { + HWVoiceOut hw; + int exit; + int initialized; + Audiodev *dev; + SDL_AudioDeviceID devid; +} SDLVoiceOut; + +typedef struct SDLVoiceIn { + HWVoiceIn hw; + int exit; + int initialized; + Audiodev *dev; + SDL_AudioDeviceID devid; +} SDLVoiceIn; + +static void GCC_FMT_ATTR (1, 2) sdl_logerr (const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + AUD_vlog (AUDIO_CAP, fmt, ap); + va_end (ap); + + AUD_log (AUDIO_CAP, "Reason: %s\n", SDL_GetError ()); +} + +static int aud_to_sdlfmt (AudioFormat fmt) +{ + switch (fmt) { + case AUDIO_FORMAT_S8: + return AUDIO_S8; + + case AUDIO_FORMAT_U8: + return AUDIO_U8; + + case AUDIO_FORMAT_S16: + return AUDIO_S16LSB; + + case AUDIO_FORMAT_U16: + return AUDIO_U16LSB; + + case AUDIO_FORMAT_S32: + return AUDIO_S32LSB; + + /* no unsigned 32-bit support in SDL */ + + case AUDIO_FORMAT_F32: + return AUDIO_F32LSB; + + default: + dolog ("Internal logic error: Bad audio format %d\n", fmt); +#ifdef DEBUG_AUDIO + abort (); +#endif + return AUDIO_U8; + } +} + +static int sdl_to_audfmt(int sdlfmt, AudioFormat *fmt, int *endianness) +{ + switch (sdlfmt) { + case AUDIO_S8: + *endianness = 0; + *fmt = AUDIO_FORMAT_S8; + break; + + case AUDIO_U8: + *endianness = 0; + *fmt = AUDIO_FORMAT_U8; + break; + + case AUDIO_S16LSB: + *endianness = 0; + *fmt = AUDIO_FORMAT_S16; + break; + + case AUDIO_U16LSB: + *endianness = 0; + *fmt = AUDIO_FORMAT_U16; + break; + + case AUDIO_S16MSB: + *endianness = 1; + *fmt = AUDIO_FORMAT_S16; + break; + + case AUDIO_U16MSB: + *endianness = 1; + *fmt = AUDIO_FORMAT_U16; + break; + + case AUDIO_S32LSB: + *endianness = 0; + *fmt = AUDIO_FORMAT_S32; + break; + + case AUDIO_S32MSB: + *endianness = 1; + *fmt = AUDIO_FORMAT_S32; + break; + + case AUDIO_F32LSB: + *endianness = 0; + *fmt = AUDIO_FORMAT_F32; + break; + + case AUDIO_F32MSB: + *endianness = 1; + *fmt = AUDIO_FORMAT_F32; + break; + + default: + dolog ("Unrecognized SDL audio format %d\n", sdlfmt); + return -1; + } + + return 0; +} + +static SDL_AudioDeviceID sdl_open(SDL_AudioSpec *req, SDL_AudioSpec *obt, + int rec) +{ + SDL_AudioDeviceID devid; +#ifndef _WIN32 + int err; + sigset_t new, old; + + /* Make sure potential threads created by SDL don't hog signals. */ + err = sigfillset (&new); + if (err) { + dolog ("sdl_open: sigfillset failed: %s\n", strerror (errno)); + return 0; + } + err = pthread_sigmask (SIG_BLOCK, &new, &old); + if (err) { + dolog ("sdl_open: pthread_sigmask failed: %s\n", strerror (err)); + return 0; + } +#endif + + devid = SDL_OpenAudioDevice(NULL, rec, req, obt, 0); + if (!devid) { + sdl_logerr("SDL_OpenAudioDevice for %s failed\n", + rec ? "recording" : "playback"); + } + +#ifndef _WIN32 + err = pthread_sigmask (SIG_SETMASK, &old, NULL); + if (err) { + dolog ("sdl_open: pthread_sigmask (restore) failed: %s\n", + strerror (errno)); + /* We have failed to restore original signal mask, all bets are off, + so exit the process */ + exit (EXIT_FAILURE); + } +#endif + return devid; +} + +static void sdl_close_out(SDLVoiceOut *sdl) +{ + if (sdl->initialized) { + SDL_LockAudioDevice(sdl->devid); + sdl->exit = 1; + SDL_UnlockAudioDevice(sdl->devid); + SDL_PauseAudioDevice(sdl->devid, 1); + sdl->initialized = 0; + } + if (sdl->devid) { + SDL_CloseAudioDevice(sdl->devid); + sdl->devid = 0; + } +} + +static void sdl_callback_out(void *opaque, Uint8 *buf, int len) +{ + SDLVoiceOut *sdl = opaque; + HWVoiceOut *hw = &sdl->hw; + + if (!sdl->exit) { + + /* dolog("callback_out: len=%d avail=%zu\n", len, hw->pending_emul); */ + + while (hw->pending_emul && len) { + size_t write_len; + ssize_t start = (ssize_t)hw->pos_emul - hw->pending_emul; + if (start < 0) { + start += hw->size_emul; + } + assert(start >= 0 && start < hw->size_emul); + + write_len = MIN(MIN(hw->pending_emul, len), + hw->size_emul - start); + + memcpy(buf, hw->buf_emul + start, write_len); + hw->pending_emul -= write_len; + len -= write_len; + buf += write_len; + } + } + + /* clear remaining buffer that we couldn't fill with data */ + if (len) { + audio_pcm_info_clear_buf(&hw->info, buf, + len / hw->info.bytes_per_frame); + } +} + +static void sdl_close_in(SDLVoiceIn *sdl) +{ + if (sdl->initialized) { + SDL_LockAudioDevice(sdl->devid); + sdl->exit = 1; + SDL_UnlockAudioDevice(sdl->devid); + SDL_PauseAudioDevice(sdl->devid, 1); + sdl->initialized = 0; + } + if (sdl->devid) { + SDL_CloseAudioDevice(sdl->devid); + sdl->devid = 0; + } +} + +static void sdl_callback_in(void *opaque, Uint8 *buf, int len) +{ + SDLVoiceIn *sdl = opaque; + HWVoiceIn *hw = &sdl->hw; + + if (sdl->exit) { + return; + } + + /* dolog("callback_in: len=%d pending=%zu\n", len, hw->pending_emul); */ + + while (hw->pending_emul < hw->size_emul && len) { + size_t read_len = MIN(len, MIN(hw->size_emul - hw->pos_emul, + hw->size_emul - hw->pending_emul)); + + memcpy(hw->buf_emul + hw->pos_emul, buf, read_len); + + hw->pending_emul += read_len; + hw->pos_emul = (hw->pos_emul + read_len) % hw->size_emul; + len -= read_len; + buf += read_len; + } +} + +#define SDL_WRAPPER_FUNC(name, ret_type, args_decl, args, dir) \ + static ret_type glue(sdl_, name)args_decl \ + { \ + ret_type ret; \ + glue(SDLVoice, dir) *sdl = (glue(SDLVoice, dir) *)hw; \ + \ + SDL_LockAudioDevice(sdl->devid); \ + ret = glue(audio_generic_, name)args; \ + SDL_UnlockAudioDevice(sdl->devid); \ + \ + return ret; \ + } + +#define SDL_WRAPPER_VOID_FUNC(name, args_decl, args, dir) \ + static void glue(sdl_, name)args_decl \ + { \ + glue(SDLVoice, dir) *sdl = (glue(SDLVoice, dir) *)hw; \ + \ + SDL_LockAudioDevice(sdl->devid); \ + glue(audio_generic_, name)args; \ + SDL_UnlockAudioDevice(sdl->devid); \ + } + +SDL_WRAPPER_FUNC(get_buffer_out, void *, (HWVoiceOut *hw, size_t *size), + (hw, size), Out) +SDL_WRAPPER_FUNC(put_buffer_out, size_t, + (HWVoiceOut *hw, void *buf, size_t size), (hw, buf, size), Out) +SDL_WRAPPER_FUNC(write, size_t, + (HWVoiceOut *hw, void *buf, size_t size), (hw, buf, size), Out) +SDL_WRAPPER_FUNC(read, size_t, (HWVoiceIn *hw, void *buf, size_t size), + (hw, buf, size), In) +SDL_WRAPPER_FUNC(get_buffer_in, void *, (HWVoiceIn *hw, size_t *size), + (hw, size), In) +SDL_WRAPPER_VOID_FUNC(put_buffer_in, (HWVoiceIn *hw, void *buf, size_t size), + (hw, buf, size), In) +#undef SDL_WRAPPER_FUNC +#undef SDL_WRAPPER_VOID_FUNC + +static void sdl_fini_out(HWVoiceOut *hw) +{ + SDLVoiceOut *sdl = (SDLVoiceOut *)hw; + + sdl_close_out(sdl); +} + +static int sdl_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + SDLVoiceOut *sdl = (SDLVoiceOut *)hw; + SDL_AudioSpec req, obt; + int endianness; + int err; + AudioFormat effective_fmt; + Audiodev *dev = drv_opaque; + AudiodevSdlPerDirectionOptions *spdo = dev->u.sdl.out; + struct audsettings obt_as; + + req.freq = as->freq; + req.format = aud_to_sdlfmt (as->fmt); + req.channels = as->nchannels; + /* + * This is wrong. SDL samples are QEMU frames. The buffer size will be + * the requested buffer size multiplied by the number of channels. + */ + req.samples = audio_buffer_samples( + qapi_AudiodevSdlPerDirectionOptions_base(spdo), as, 11610); + req.callback = sdl_callback_out; + req.userdata = sdl; + + sdl->dev = dev; + sdl->devid = sdl_open(&req, &obt, 0); + if (!sdl->devid) { + return -1; + } + + err = sdl_to_audfmt(obt.format, &effective_fmt, &endianness); + if (err) { + sdl_close_out(sdl); + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.channels; + obt_as.fmt = effective_fmt; + obt_as.endianness = endianness; + + audio_pcm_init_info (&hw->info, &obt_as); + hw->samples = (spdo->has_buffer_count ? spdo->buffer_count : 4) * + obt.samples; + + sdl->initialized = 1; + sdl->exit = 0; + return 0; +} + +static void sdl_enable_out(HWVoiceOut *hw, bool enable) +{ + SDLVoiceOut *sdl = (SDLVoiceOut *)hw; + + SDL_PauseAudioDevice(sdl->devid, !enable); +} + +static void sdl_fini_in(HWVoiceIn *hw) +{ + SDLVoiceIn *sdl = (SDLVoiceIn *)hw; + + sdl_close_in(sdl); +} + +static int sdl_init_in(HWVoiceIn *hw, audsettings *as, void *drv_opaque) +{ + SDLVoiceIn *sdl = (SDLVoiceIn *)hw; + SDL_AudioSpec req, obt; + int endianness; + int err; + AudioFormat effective_fmt; + Audiodev *dev = drv_opaque; + AudiodevSdlPerDirectionOptions *spdo = dev->u.sdl.in; + struct audsettings obt_as; + + req.freq = as->freq; + req.format = aud_to_sdlfmt(as->fmt); + req.channels = as->nchannels; + /* SDL samples are QEMU frames */ + req.samples = audio_buffer_frames( + qapi_AudiodevSdlPerDirectionOptions_base(spdo), as, 11610); + req.callback = sdl_callback_in; + req.userdata = sdl; + + sdl->dev = dev; + sdl->devid = sdl_open(&req, &obt, 1); + if (!sdl->devid) { + return -1; + } + + err = sdl_to_audfmt(obt.format, &effective_fmt, &endianness); + if (err) { + sdl_close_in(sdl); + return -1; + } + + obt_as.freq = obt.freq; + obt_as.nchannels = obt.channels; + obt_as.fmt = effective_fmt; + obt_as.endianness = endianness; + + audio_pcm_init_info(&hw->info, &obt_as); + hw->samples = (spdo->has_buffer_count ? spdo->buffer_count : 4) * + obt.samples; + hw->size_emul = hw->samples * hw->info.bytes_per_frame; + hw->buf_emul = g_malloc(hw->size_emul); + hw->pos_emul = hw->pending_emul = 0; + + sdl->initialized = 1; + sdl->exit = 0; + return 0; +} + +static void sdl_enable_in(HWVoiceIn *hw, bool enable) +{ + SDLVoiceIn *sdl = (SDLVoiceIn *)hw; + + SDL_PauseAudioDevice(sdl->devid, !enable); +} + +static void *sdl_audio_init(Audiodev *dev) +{ + if (SDL_InitSubSystem (SDL_INIT_AUDIO)) { + sdl_logerr ("SDL failed to initialize audio subsystem\n"); + return NULL; + } + + return dev; +} + +static void sdl_audio_fini (void *opaque) +{ + SDL_QuitSubSystem (SDL_INIT_AUDIO); +} + +static struct audio_pcm_ops sdl_pcm_ops = { + .init_out = sdl_init_out, + .fini_out = sdl_fini_out, + /* wrapper for audio_generic_write */ + .write = sdl_write, + /* wrapper for audio_generic_get_buffer_out */ + .get_buffer_out = sdl_get_buffer_out, + /* wrapper for audio_generic_put_buffer_out */ + .put_buffer_out = sdl_put_buffer_out, + .enable_out = sdl_enable_out, + .init_in = sdl_init_in, + .fini_in = sdl_fini_in, + /* wrapper for audio_generic_read */ + .read = sdl_read, + /* wrapper for audio_generic_get_buffer_in */ + .get_buffer_in = sdl_get_buffer_in, + /* wrapper for audio_generic_put_buffer_in */ + .put_buffer_in = sdl_put_buffer_in, + .enable_in = sdl_enable_in, +}; + +static struct audio_driver sdl_audio_driver = { + .name = "sdl", + .descr = "SDL http://www.libsdl.org", + .init = sdl_audio_init, + .fini = sdl_audio_fini, + .pcm_ops = &sdl_pcm_ops, + .can_be_default = 1, + .max_voices_out = INT_MAX, + .max_voices_in = INT_MAX, + .voice_size_out = sizeof(SDLVoiceOut), + .voice_size_in = sizeof(SDLVoiceIn), +}; + +static void register_audio_sdl(void) +{ + audio_driver_register(&sdl_audio_driver); +} +type_init(register_audio_sdl); diff --git a/audio/spiceaudio.c b/audio/spiceaudio.c new file mode 100644 index 000000000..a8d370fe6 --- /dev/null +++ b/audio/spiceaudio.c @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2010 Red Hat, Inc. + * + * maintained by Gerd Hoffmann <kraxel@redhat.com> + * + * 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 or + * (at your option) version 3 of the License. + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" +#include "qemu/error-report.h" +#include "qemu/timer.h" +#include "ui/qemu-spice.h" + +#define AUDIO_CAP "spice" +#include "audio.h" +#include "audio_int.h" + +#if SPICE_INTERFACE_PLAYBACK_MAJOR > 1 || SPICE_INTERFACE_PLAYBACK_MINOR >= 3 +#define LINE_OUT_SAMPLES (480 * 4) +#else +#define LINE_OUT_SAMPLES (256 * 4) +#endif + +#if SPICE_INTERFACE_RECORD_MAJOR > 2 || SPICE_INTERFACE_RECORD_MINOR >= 3 +#define LINE_IN_SAMPLES (480 * 4) +#else +#define LINE_IN_SAMPLES (256 * 4) +#endif + +typedef struct SpiceVoiceOut { + HWVoiceOut hw; + SpicePlaybackInstance sin; + RateCtl rate; + int active; + uint32_t *frame; + uint32_t fpos; + uint32_t fsize; +} SpiceVoiceOut; + +typedef struct SpiceVoiceIn { + HWVoiceIn hw; + SpiceRecordInstance sin; + RateCtl rate; + int active; +} SpiceVoiceIn; + +static const SpicePlaybackInterface playback_sif = { + .base.type = SPICE_INTERFACE_PLAYBACK, + .base.description = "playback", + .base.major_version = SPICE_INTERFACE_PLAYBACK_MAJOR, + .base.minor_version = SPICE_INTERFACE_PLAYBACK_MINOR, +}; + +static const SpiceRecordInterface record_sif = { + .base.type = SPICE_INTERFACE_RECORD, + .base.description = "record", + .base.major_version = SPICE_INTERFACE_RECORD_MAJOR, + .base.minor_version = SPICE_INTERFACE_RECORD_MINOR, +}; + +static void *spice_audio_init(Audiodev *dev) +{ + if (!using_spice) { + return NULL; + } + return &spice_audio_init; +} + +static void spice_audio_fini (void *opaque) +{ + /* nothing */ +} + +/* playback */ + +static int line_out_init(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + SpiceVoiceOut *out = container_of (hw, SpiceVoiceOut, hw); + struct audsettings settings; + +#if SPICE_INTERFACE_PLAYBACK_MAJOR > 1 || SPICE_INTERFACE_PLAYBACK_MINOR >= 3 + settings.freq = spice_server_get_best_playback_rate(NULL); +#else + settings.freq = SPICE_INTERFACE_PLAYBACK_FREQ; +#endif + settings.nchannels = SPICE_INTERFACE_PLAYBACK_CHAN; + settings.fmt = AUDIO_FORMAT_S16; + settings.endianness = AUDIO_HOST_ENDIANNESS; + + audio_pcm_init_info (&hw->info, &settings); + hw->samples = LINE_OUT_SAMPLES; + out->active = 0; + + out->sin.base.sif = &playback_sif.base; + qemu_spice.add_interface(&out->sin.base); +#if SPICE_INTERFACE_PLAYBACK_MAJOR > 1 || SPICE_INTERFACE_PLAYBACK_MINOR >= 3 + spice_server_set_playback_rate(&out->sin, settings.freq); +#endif + return 0; +} + +static void line_out_fini (HWVoiceOut *hw) +{ + SpiceVoiceOut *out = container_of (hw, SpiceVoiceOut, hw); + + spice_server_remove_interface (&out->sin.base); +} + +static void *line_out_get_buffer(HWVoiceOut *hw, size_t *size) +{ + SpiceVoiceOut *out = container_of(hw, SpiceVoiceOut, hw); + + if (!out->frame) { + spice_server_playback_get_buffer(&out->sin, &out->frame, &out->fsize); + out->fpos = 0; + } + + if (out->frame) { + *size = MIN((out->fsize - out->fpos) << 2, *size); + } + + *size = audio_rate_get_bytes(&hw->info, &out->rate, *size); + + return out->frame + out->fpos; +} + +static size_t line_out_put_buffer(HWVoiceOut *hw, void *buf, size_t size) +{ + SpiceVoiceOut *out = container_of(hw, SpiceVoiceOut, hw); + + if (buf) { + assert(buf == out->frame + out->fpos && out->fpos <= out->fsize); + out->fpos += size >> 2; + + if (out->fpos == out->fsize) { /* buffer full */ + spice_server_playback_put_samples(&out->sin, out->frame); + out->frame = NULL; + } + } + + return size; +} + +static void line_out_enable(HWVoiceOut *hw, bool enable) +{ + SpiceVoiceOut *out = container_of (hw, SpiceVoiceOut, hw); + + if (enable) { + if (out->active) { + return; + } + out->active = 1; + audio_rate_start(&out->rate); + spice_server_playback_start (&out->sin); + } else { + if (!out->active) { + return; + } + out->active = 0; + if (out->frame) { + memset(out->frame + out->fpos, 0, (out->fsize - out->fpos) << 2); + spice_server_playback_put_samples (&out->sin, out->frame); + out->frame = NULL; + } + spice_server_playback_stop (&out->sin); + } +} + +#if ((SPICE_INTERFACE_PLAYBACK_MAJOR >= 1) && (SPICE_INTERFACE_PLAYBACK_MINOR >= 2)) +static void line_out_volume(HWVoiceOut *hw, Volume *vol) +{ + SpiceVoiceOut *out = container_of(hw, SpiceVoiceOut, hw); + uint16_t svol[2]; + + assert(vol->channels == 2); + svol[0] = vol->vol[0] * 257; + svol[1] = vol->vol[1] * 257; + spice_server_playback_set_volume(&out->sin, 2, svol); + spice_server_playback_set_mute(&out->sin, vol->mute); +} +#endif + +/* record */ + +static int line_in_init(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque) +{ + SpiceVoiceIn *in = container_of (hw, SpiceVoiceIn, hw); + struct audsettings settings; + +#if SPICE_INTERFACE_RECORD_MAJOR > 2 || SPICE_INTERFACE_RECORD_MINOR >= 3 + settings.freq = spice_server_get_best_record_rate(NULL); +#else + settings.freq = SPICE_INTERFACE_RECORD_FREQ; +#endif + settings.nchannels = SPICE_INTERFACE_RECORD_CHAN; + settings.fmt = AUDIO_FORMAT_S16; + settings.endianness = AUDIO_HOST_ENDIANNESS; + + audio_pcm_init_info (&hw->info, &settings); + hw->samples = LINE_IN_SAMPLES; + in->active = 0; + + in->sin.base.sif = &record_sif.base; + qemu_spice.add_interface(&in->sin.base); +#if SPICE_INTERFACE_RECORD_MAJOR > 2 || SPICE_INTERFACE_RECORD_MINOR >= 3 + spice_server_set_record_rate(&in->sin, settings.freq); +#endif + return 0; +} + +static void line_in_fini (HWVoiceIn *hw) +{ + SpiceVoiceIn *in = container_of (hw, SpiceVoiceIn, hw); + + spice_server_remove_interface (&in->sin.base); +} + +static size_t line_in_read(HWVoiceIn *hw, void *buf, size_t len) +{ + SpiceVoiceIn *in = container_of (hw, SpiceVoiceIn, hw); + uint64_t to_read = audio_rate_get_bytes(&hw->info, &in->rate, len) >> 2; + size_t ready = spice_server_record_get_samples(&in->sin, buf, to_read); + + /* XXX: do we need this? */ + if (ready == 0) { + memset(buf, 0, to_read << 2); + ready = to_read; + } + + return ready << 2; +} + +static void line_in_enable(HWVoiceIn *hw, bool enable) +{ + SpiceVoiceIn *in = container_of (hw, SpiceVoiceIn, hw); + + if (enable) { + if (in->active) { + return; + } + in->active = 1; + audio_rate_start(&in->rate); + spice_server_record_start (&in->sin); + } else { + if (!in->active) { + return; + } + in->active = 0; + spice_server_record_stop (&in->sin); + } +} + +#if ((SPICE_INTERFACE_RECORD_MAJOR >= 2) && (SPICE_INTERFACE_RECORD_MINOR >= 2)) +static void line_in_volume(HWVoiceIn *hw, Volume *vol) +{ + SpiceVoiceIn *in = container_of(hw, SpiceVoiceIn, hw); + uint16_t svol[2]; + + assert(vol->channels == 2); + svol[0] = vol->vol[0] * 257; + svol[1] = vol->vol[1] * 257; + spice_server_record_set_volume(&in->sin, 2, svol); + spice_server_record_set_mute(&in->sin, vol->mute); +} +#endif + +static struct audio_pcm_ops audio_callbacks = { + .init_out = line_out_init, + .fini_out = line_out_fini, + .write = audio_generic_write, + .get_buffer_out = line_out_get_buffer, + .put_buffer_out = line_out_put_buffer, + .enable_out = line_out_enable, +#if (SPICE_INTERFACE_PLAYBACK_MAJOR >= 1) && \ + (SPICE_INTERFACE_PLAYBACK_MINOR >= 2) + .volume_out = line_out_volume, +#endif + + .init_in = line_in_init, + .fini_in = line_in_fini, + .read = line_in_read, + .run_buffer_in = audio_generic_run_buffer_in, + .enable_in = line_in_enable, +#if ((SPICE_INTERFACE_RECORD_MAJOR >= 2) && (SPICE_INTERFACE_RECORD_MINOR >= 2)) + .volume_in = line_in_volume, +#endif +}; + +static struct audio_driver spice_audio_driver = { + .name = "spice", + .descr = "spice audio driver", + .init = spice_audio_init, + .fini = spice_audio_fini, + .pcm_ops = &audio_callbacks, + .max_voices_out = 1, + .max_voices_in = 1, + .voice_size_out = sizeof (SpiceVoiceOut), + .voice_size_in = sizeof (SpiceVoiceIn), +}; + +static void register_audio_spice(void) +{ + audio_driver_register(&spice_audio_driver); +} +type_init(register_audio_spice); + +module_dep("ui-spice-core"); diff --git a/audio/trace-events b/audio/trace-events new file mode 100644 index 000000000..957c92337 --- /dev/null +++ b/audio/trace-events @@ -0,0 +1,19 @@ +# See docs/devel/tracing.rst for syntax documentation. + +# alsaaudio.c +alsa_revents(int revents) "revents = %d" +alsa_pollout(int i, int fd) "i = %d fd = %d" +alsa_set_handler(int events, int index, int fd, int err) "events=0x%x index=%d fd=%d err=%d" +alsa_wrote_zero(int len) "Failed to write %d frames (wrote zero)" +alsa_read_zero(long len) "Failed to read %ld frames (read zero)" +alsa_xrun_out(void) "Recovering from playback xrun" +alsa_xrun_in(void) "Recovering from capture xrun" +alsa_resume_out(void) "Resuming suspended output stream" + +# ossaudio.c +oss_version(int version) "OSS version = 0x%x" + +# audio.c +audio_timer_start(int interval) "interval %d ms" +audio_timer_stop(void) "" +audio_timer_delayed(int interval) "interval %d ms" diff --git a/audio/trace.h b/audio/trace.h new file mode 100644 index 000000000..4072a11b0 --- /dev/null +++ b/audio/trace.h @@ -0,0 +1 @@ +#include "trace/trace-audio.h" diff --git a/audio/wavaudio.c b/audio/wavaudio.c new file mode 100644 index 000000000..20e6853f8 --- /dev/null +++ b/audio/wavaudio.c @@ -0,0 +1,221 @@ +/* + * QEMU WAV audio driver + * + * Copyright (c) 2004-2005 Vassili Karpov (malc) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/host-utils.h" +#include "qemu/module.h" +#include "qemu/timer.h" +#include "qapi/opts-visitor.h" +#include "audio.h" + +#define AUDIO_CAP "wav" +#include "audio_int.h" + +typedef struct WAVVoiceOut { + HWVoiceOut hw; + FILE *f; + RateCtl rate; + int total_samples; +} WAVVoiceOut; + +static size_t wav_write_out(HWVoiceOut *hw, void *buf, size_t len) +{ + WAVVoiceOut *wav = (WAVVoiceOut *) hw; + int64_t bytes = audio_rate_get_bytes(&hw->info, &wav->rate, len); + assert(bytes % hw->info.bytes_per_frame == 0); + + if (bytes && fwrite(buf, bytes, 1, wav->f) != 1) { + dolog("wav_write_out: fwrite of %" PRId64 " bytes failed\nReason: %s\n", + bytes, strerror(errno)); + } + + wav->total_samples += bytes / hw->info.bytes_per_frame; + return bytes; +} + +/* VICE code: Store number as little endian. */ +static void le_store (uint8_t *buf, uint32_t val, int len) +{ + int i; + for (i = 0; i < len; i++) { + buf[i] = (uint8_t) (val & 0xff); + val >>= 8; + } +} + +static int wav_init_out(HWVoiceOut *hw, struct audsettings *as, + void *drv_opaque) +{ + WAVVoiceOut *wav = (WAVVoiceOut *) hw; + int bits16 = 0, stereo = 0; + uint8_t hdr[] = { + 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, + 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x44, 0xac, 0x00, 0x00, 0x10, 0xb1, 0x02, 0x00, 0x04, + 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00 + }; + Audiodev *dev = drv_opaque; + AudiodevWavOptions *wopts = &dev->u.wav; + struct audsettings wav_as = audiodev_to_audsettings(dev->u.wav.out); + const char *wav_path = wopts->has_path ? wopts->path : "qemu.wav"; + + stereo = wav_as.nchannels == 2; + switch (wav_as.fmt) { + case AUDIO_FORMAT_S8: + case AUDIO_FORMAT_U8: + bits16 = 0; + break; + + case AUDIO_FORMAT_S16: + case AUDIO_FORMAT_U16: + bits16 = 1; + break; + + case AUDIO_FORMAT_S32: + case AUDIO_FORMAT_U32: + dolog ("WAVE files can not handle 32bit formats\n"); + return -1; + + default: + abort(); + } + + hdr[34] = bits16 ? 0x10 : 0x08; + + wav_as.endianness = 0; + audio_pcm_init_info (&hw->info, &wav_as); + + hw->samples = 1024; + le_store (hdr + 22, hw->info.nchannels, 2); + le_store (hdr + 24, hw->info.freq, 4); + le_store (hdr + 28, hw->info.freq << (bits16 + stereo), 4); + le_store (hdr + 32, 1 << (bits16 + stereo), 2); + + wav->f = fopen(wav_path, "wb"); + if (!wav->f) { + dolog ("Failed to open wave file `%s'\nReason: %s\n", + wav_path, strerror(errno)); + return -1; + } + + if (fwrite (hdr, sizeof (hdr), 1, wav->f) != 1) { + dolog ("wav_init_out: failed to write header\nReason: %s\n", + strerror(errno)); + return -1; + } + + audio_rate_start(&wav->rate); + return 0; +} + +static void wav_fini_out (HWVoiceOut *hw) +{ + WAVVoiceOut *wav = (WAVVoiceOut *) hw; + uint8_t rlen[4]; + uint8_t dlen[4]; + uint32_t datalen = wav->total_samples * hw->info.bytes_per_frame; + uint32_t rifflen = datalen + 36; + + if (!wav->f) { + return; + } + + le_store (rlen, rifflen, 4); + le_store (dlen, datalen, 4); + + if (fseek (wav->f, 4, SEEK_SET)) { + dolog ("wav_fini_out: fseek to rlen failed\nReason: %s\n", + strerror(errno)); + goto doclose; + } + if (fwrite (rlen, 4, 1, wav->f) != 1) { + dolog ("wav_fini_out: failed to write rlen\nReason: %s\n", + strerror (errno)); + goto doclose; + } + if (fseek (wav->f, 32, SEEK_CUR)) { + dolog ("wav_fini_out: fseek to dlen failed\nReason: %s\n", + strerror (errno)); + goto doclose; + } + if (fwrite (dlen, 4, 1, wav->f) != 1) { + dolog ("wav_fini_out: failed to write dlen\nReaons: %s\n", + strerror (errno)); + goto doclose; + } + + doclose: + if (fclose (wav->f)) { + dolog ("wav_fini_out: fclose %p failed\nReason: %s\n", + wav->f, strerror (errno)); + } + wav->f = NULL; +} + +static void wav_enable_out(HWVoiceOut *hw, bool enable) +{ + WAVVoiceOut *wav = (WAVVoiceOut *) hw; + + if (enable) { + audio_rate_start(&wav->rate); + } +} + +static void *wav_audio_init(Audiodev *dev) +{ + assert(dev->driver == AUDIODEV_DRIVER_WAV); + return dev; +} + +static void wav_audio_fini (void *opaque) +{ + ldebug ("wav_fini"); +} + +static struct audio_pcm_ops wav_pcm_ops = { + .init_out = wav_init_out, + .fini_out = wav_fini_out, + .write = wav_write_out, + .run_buffer_out = audio_generic_run_buffer_out, + .enable_out = wav_enable_out, +}; + +static struct audio_driver wav_audio_driver = { + .name = "wav", + .descr = "WAV renderer http://wikipedia.org/wiki/WAV", + .init = wav_audio_init, + .fini = wav_audio_fini, + .pcm_ops = &wav_pcm_ops, + .can_be_default = 0, + .max_voices_out = 1, + .max_voices_in = 0, + .voice_size_out = sizeof (WAVVoiceOut), + .voice_size_in = 0 +}; + +static void register_audio_wav(void) +{ + audio_driver_register(&wav_audio_driver); +} +type_init(register_audio_wav); diff --git a/audio/wavcapture.c b/audio/wavcapture.c new file mode 100644 index 000000000..c60286e16 --- /dev/null +++ b/audio/wavcapture.c @@ -0,0 +1,191 @@ +#include "qemu/osdep.h" +#include "qemu/qemu-print.h" +#include "qapi/error.h" +#include "qemu/error-report.h" +#include "audio.h" + +typedef struct { + FILE *f; + int bytes; + char *path; + int freq; + int bits; + int nchannels; + CaptureVoiceOut *cap; +} WAVState; + +/* VICE code: Store number as little endian. */ +static void le_store (uint8_t *buf, uint32_t val, int len) +{ + int i; + for (i = 0; i < len; i++) { + buf[i] = (uint8_t) (val & 0xff); + val >>= 8; + } +} + +static void wav_notify (void *opaque, audcnotification_e cmd) +{ + (void) opaque; + (void) cmd; +} + +static void wav_destroy (void *opaque) +{ + WAVState *wav = opaque; + uint8_t rlen[4]; + uint8_t dlen[4]; + uint32_t datalen = wav->bytes; + uint32_t rifflen = datalen + 36; + + if (wav->f) { + le_store (rlen, rifflen, 4); + le_store (dlen, datalen, 4); + + if (fseek (wav->f, 4, SEEK_SET)) { + error_report("wav_destroy: rlen fseek failed: %s", + strerror(errno)); + goto doclose; + } + if (fwrite (rlen, 4, 1, wav->f) != 1) { + error_report("wav_destroy: rlen fwrite failed: %s", + strerror(errno)); + goto doclose; + } + if (fseek (wav->f, 32, SEEK_CUR)) { + error_report("wav_destroy: dlen fseek failed: %s", + strerror(errno)); + goto doclose; + } + if (fwrite (dlen, 1, 4, wav->f) != 4) { + error_report("wav_destroy: dlen fwrite failed: %s", + strerror(errno)); + goto doclose; + } + doclose: + if (fclose (wav->f)) { + error_report("wav_destroy: fclose failed: %s", strerror(errno)); + } + } + + g_free (wav->path); +} + +static void wav_capture(void *opaque, const void *buf, int size) +{ + WAVState *wav = opaque; + + if (fwrite (buf, size, 1, wav->f) != 1) { + error_report("wav_capture: fwrite error: %s", strerror(errno)); + } + wav->bytes += size; +} + +static void wav_capture_destroy (void *opaque) +{ + WAVState *wav = opaque; + + AUD_del_capture (wav->cap, wav); + g_free (wav); +} + +static void wav_capture_info (void *opaque) +{ + WAVState *wav = opaque; + char *path = wav->path; + + qemu_printf("Capturing audio(%d,%d,%d) to %s: %d bytes\n", + wav->freq, wav->bits, wav->nchannels, + path ? path : "<not available>", wav->bytes); +} + +static struct capture_ops wav_capture_ops = { + .destroy = wav_capture_destroy, + .info = wav_capture_info +}; + +int wav_start_capture(AudioState *state, CaptureState *s, const char *path, + int freq, int bits, int nchannels) +{ + WAVState *wav; + uint8_t hdr[] = { + 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, + 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x02, 0x00, 0x44, 0xac, 0x00, 0x00, 0x10, 0xb1, 0x02, 0x00, 0x04, + 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00 + }; + struct audsettings as; + struct audio_capture_ops ops; + int stereo, bits16, shift; + CaptureVoiceOut *cap; + + if (bits != 8 && bits != 16) { + error_report("incorrect bit count %d, must be 8 or 16", bits); + return -1; + } + + if (nchannels != 1 && nchannels != 2) { + error_report("incorrect channel count %d, must be 1 or 2", + nchannels); + return -1; + } + + stereo = nchannels == 2; + bits16 = bits == 16; + + as.freq = freq; + as.nchannels = 1 << stereo; + as.fmt = bits16 ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U8; + as.endianness = 0; + + ops.notify = wav_notify; + ops.capture = wav_capture; + ops.destroy = wav_destroy; + + wav = g_malloc0 (sizeof (*wav)); + + shift = bits16 + stereo; + hdr[34] = bits16 ? 0x10 : 0x08; + + le_store (hdr + 22, as.nchannels, 2); + le_store (hdr + 24, freq, 4); + le_store (hdr + 28, freq << shift, 4); + le_store (hdr + 32, 1 << shift, 2); + + wav->f = fopen (path, "wb"); + if (!wav->f) { + error_report("Failed to open wave file `%s': %s", + path, strerror(errno)); + g_free (wav); + return -1; + } + + wav->path = g_strdup (path); + wav->bits = bits; + wav->nchannels = nchannels; + wav->freq = freq; + + if (fwrite (hdr, sizeof (hdr), 1, wav->f) != 1) { + error_report("Failed to write header: %s", strerror(errno)); + goto error_free; + } + + cap = AUD_add_capture(state, &as, &ops, wav); + if (!cap) { + error_report("Failed to add audio capture"); + goto error_free; + } + + wav->cap = cap; + s->opaque = wav; + s->ops = wav_capture_ops; + return 0; + +error_free: + g_free (wav->path); + if (fclose (wav->f)) { + error_report("Failed to close wave file: %s", strerror(errno)); + } + g_free (wav); + return -1; +} |