| /* |
| * SPDX-License-Identifier: MIT |
| * |
| * Copyright (c) 2003-2005 Vassili Karpov (malc) |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "qemu/audio.h" |
| #include "migration/vmstate.h" |
| #include "qemu/bswap.h" |
| #include "qemu/timer.h" |
| #include "qapi/error.h" |
| #include "qapi/clone-visitor.h" |
| #include "qapi/qobject-input-visitor.h" |
| #include "qapi/qapi-visit-audio.h" |
| #include "qapi/qapi-commands-audio.h" |
| #include "qobject/qdict.h" |
| #include "qemu/error-report.h" |
| #include "qemu/log.h" |
| #include "qemu/module.h" |
| #include "qemu/help_option.h" |
| #include "qom/object.h" |
| #include "system/system.h" |
| #include "system/replay.h" |
| #include "system/runstate.h" |
| #include "trace.h" |
| #include "trace/control.h" |
| |
| #include "audio_int.h" |
| |
| #define SW_NAME(sw) (sw)->name ? (sw)->name : "unknown" |
| |
| #define audio_bug(fmt, ...) error_report("%s: " fmt, __func__, ##__VA_ARGS__) |
| |
| 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 |
| }; |
| |
| /* |
| * Convert audio format to mixeng_clip index. Used by audio_pcm_sw_init_ and |
| * audio_mixeng_backend_add_capture() |
| */ |
| static int audio_format_to_index(AudioFormat af) |
| { |
| switch (af) { |
| case AUDIO_FORMAT_U8: |
| case AUDIO_FORMAT_S8: |
| return 0; |
| case AUDIO_FORMAT_U16: |
| case AUDIO_FORMAT_S16: |
| return 1; |
| case AUDIO_FORMAT_U32: |
| case AUDIO_FORMAT_S32: |
| return 2; |
| case AUDIO_FORMAT_F32: |
| case AUDIO_FORMAT__MAX: |
| break; |
| } |
| |
| g_assert_not_reached(); |
| } |
| |
| static char *audsettings_to_string(const struct audsettings *as) |
| { |
| return g_strdup_printf("frequency=%d nchannels=%d fmt=%s endian=%s", |
| as->freq, as->nchannels, AudioFormat_str(as->fmt), |
| as->big_endian ? "big" : "little"); |
| } |
| |
| static int audio_validate_settings (const struct audsettings *as) |
| { |
| int invalid; |
| |
| invalid = as->nchannels < 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, const struct audsettings *as) |
| { |
| return info->af == as->fmt |
| && info->freq == as->freq |
| && info->nchannels == as->nchannels |
| && info->swap_endianness == (as->big_endian != HOST_BIG_ENDIAN); |
| } |
| |
| void audio_pcm_init_info (struct audio_pcm_info *info, const struct audsettings *as) |
| { |
| info->af = as->fmt; |
| info->freq = as->freq; |
| info->nchannels = as->nchannels; |
| info->bytes_per_frame = as->nchannels * audio_format_bits(as->fmt) / 8; |
| info->bytes_per_second = info->freq * info->bytes_per_frame; |
| info->swap_endianness = (as->big_endian != HOST_BIG_ENDIAN); |
| } |
| |
| void audio_pcm_info_clear_buf(struct audio_pcm_info *info, void *buf, int len) |
| { |
| if (!len) { |
| return; |
| } |
| |
| switch (info->af) { |
| case AUDIO_FORMAT_U8: |
| memset(buf, 0x80, len * info->bytes_per_frame); |
| break; |
| case AUDIO_FORMAT_U16: { |
| 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 AUDIO_FORMAT_U32: { |
| 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; |
| } |
| case AUDIO_FORMAT_S8: |
| case AUDIO_FORMAT_S16: |
| case AUDIO_FORMAT_S32: |
| case AUDIO_FORMAT_F32: |
| memset(buf, 0x00, len * info->bytes_per_frame); |
| break; |
| case AUDIO_FORMAT__MAX: |
| g_assert_not_reached(); |
| } |
| } |
| |
| /* |
| * Capture |
| */ |
| static CaptureVoiceOut *audio_pcm_capture_find_specific(AudioMixengBackend *s, |
| const 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; |
| |
| trace_audio_notify_capture(cmd); |
| |
| 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, bool 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; |
| bool enabled = false; |
| |
| for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { |
| if (sw->active) { |
| enabled = true; |
| 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; |
| |
| g_clear_pointer(&sw->name, g_free); |
| 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) |
| { |
| AudioMixengBackend *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 = true; |
| sw->active = hw->enabled; |
| sw->vol = nominal_volume; |
| sw->rate = st_rate_start (sw->info.freq, hw_cap->info.freq); |
| QLIST_INSERT_HEAD (&hw_cap->sw_head, sw, entries); |
| QLIST_INSERT_HEAD (&hw->cap_head, sc, entries); |
| sw->name = g_strdup_printf("for %p %d,%s,%d", |
| hw, sw->info.freq, AudioFormat_str(sw->info.af), |
| sw->info.nchannels); |
| trace_audio_capture_attach(sw->name, sw->active); |
| 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 (live > hw->conv_buf.size) { |
| audio_bug("live=%zu hw->conv_buf.size=%zu", live, hw->conv_buf.size); |
| return 0; |
| } |
| return live; |
| } |
| |
| static size_t audio_pcm_hw_conv_in(HWVoiceIn *hw, void *pcm_buf, size_t samples) |
| { |
| size_t conv = 0; |
| STSampleBuffer *conv_buf = &hw->conv_buf; |
| |
| while (samples) { |
| uint8_t *src = advance(pcm_buf, conv * hw->info.bytes_per_frame); |
| size_t proc = MIN(samples, conv_buf->size - conv_buf->pos); |
| |
| hw->conv(conv_buf->buffer + conv_buf->pos, src, proc); |
| conv_buf->pos = (conv_buf->pos + proc) % conv_buf->size; |
| samples -= proc; |
| conv += proc; |
| } |
| |
| return conv; |
| } |
| |
| /* |
| * Soft voice (capture) |
| */ |
| static void audio_pcm_sw_resample_in(SWVoiceIn *sw, |
| size_t frames_in_max, size_t frames_out_max, |
| size_t *total_in, size_t *total_out) |
| { |
| HWVoiceIn *hw = sw->hw; |
| struct st_sample *src, *dst; |
| size_t live, rpos, frames_in, frames_out; |
| |
| live = hw->total_samples_captured - sw->total_hw_samples_acquired; |
| rpos = audio_ring_posb(hw->conv_buf.pos, live, hw->conv_buf.size); |
| |
| /* resample conv_buf from rpos to end of buffer */ |
| src = hw->conv_buf.buffer + rpos; |
| frames_in = MIN(frames_in_max, hw->conv_buf.size - rpos); |
| dst = sw->resample_buf.buffer; |
| frames_out = frames_out_max; |
| st_rate_flow(sw->rate, src, dst, &frames_in, &frames_out); |
| rpos += frames_in; |
| *total_in = frames_in; |
| *total_out = frames_out; |
| |
| /* resample conv_buf from start of buffer if there are input frames left */ |
| if (frames_in_max - frames_in && rpos == hw->conv_buf.size) { |
| src = hw->conv_buf.buffer; |
| frames_in = frames_in_max - frames_in; |
| dst += frames_out; |
| frames_out = frames_out_max - frames_out; |
| st_rate_flow(sw->rate, src, dst, &frames_in, &frames_out); |
| *total_in += frames_in; |
| *total_out += frames_out; |
| } |
| } |
| |
| static size_t audio_pcm_sw_read(SWVoiceIn *sw, void *buf, size_t buf_len) |
| { |
| HWVoiceIn *hw = sw->hw; |
| size_t live, frames_out_max, total_in, total_out; |
| |
| live = hw->total_samples_captured - sw->total_hw_samples_acquired; |
| if (!live) { |
| return 0; |
| } |
| if (live > hw->conv_buf.size) { |
| audio_bug("live=%zu hw->conv_buf.size=%zu", live, hw->conv_buf.size); |
| return 0; |
| } |
| |
| frames_out_max = MIN(buf_len / sw->info.bytes_per_frame, |
| sw->resample_buf.size); |
| |
| audio_pcm_sw_resample_in(sw, live, frames_out_max, &total_in, &total_out); |
| |
| if (!AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s)->volume_in) { |
| mixeng_volume(sw->resample_buf.buffer, total_out, &sw->vol); |
| } |
| sw->clip(buf, sw->resample_buf.buffer, total_out); |
| |
| sw->total_hw_samples_acquired += total_in; |
| return total_out * 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 (live > hw->mix_buf.size) { |
| audio_bug("live=%zu hw->mix_buf.size=%zu", live, hw->mix_buf.size); |
| return 0; |
| } |
| return live; |
| } |
| return 0; |
| } |
| |
| static size_t audio_pcm_hw_get_free(HWVoiceOut *hw) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| |
| return (k->buffer_get_free ? k->buffer_get_free(hw) : INT_MAX) / |
| hw->info.bytes_per_frame; |
| } |
| |
| 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.buffer + 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 (playback) |
| */ |
| static void audio_pcm_sw_resample_out(SWVoiceOut *sw, |
| size_t frames_in_max, size_t frames_out_max, |
| size_t *total_in, size_t *total_out) |
| { |
| HWVoiceOut *hw = sw->hw; |
| struct st_sample *src, *dst; |
| size_t live, wpos, frames_in, frames_out; |
| |
| live = sw->total_hw_samples_mixed; |
| wpos = (hw->mix_buf.pos + live) % hw->mix_buf.size; |
| |
| /* write to mix_buf from wpos to end of buffer */ |
| src = sw->resample_buf.buffer; |
| frames_in = frames_in_max; |
| dst = hw->mix_buf.buffer + wpos; |
| frames_out = MIN(frames_out_max, hw->mix_buf.size - wpos); |
| st_rate_flow_mix(sw->rate, src, dst, &frames_in, &frames_out); |
| wpos += frames_out; |
| *total_in = frames_in; |
| *total_out = frames_out; |
| |
| /* write to mix_buf from start of buffer if there are input frames left */ |
| if (frames_in_max - frames_in > 0 && wpos == hw->mix_buf.size) { |
| src += frames_in; |
| frames_in = frames_in_max - frames_in; |
| dst = hw->mix_buf.buffer; |
| frames_out = frames_out_max - frames_out; |
| st_rate_flow_mix(sw->rate, src, dst, &frames_in, &frames_out); |
| *total_in += frames_in; |
| *total_out += frames_out; |
| } |
| } |
| |
| static size_t audio_pcm_sw_write(SWVoiceOut *sw, void *buf, size_t buf_len) |
| { |
| HWVoiceOut *hw = sw->hw; |
| size_t live, dead, hw_free, sw_max, fe_max; |
| size_t frames_in_max, frames_out_max, total_in, total_out; |
| |
| live = sw->total_hw_samples_mixed; |
| if (live > hw->mix_buf.size) { |
| audio_bug("live=%zu hw->mix_buf.size=%zu", live, hw->mix_buf.size); |
| return 0; |
| } |
| |
| if (live == hw->mix_buf.size) { |
| trace_audio_out_full(sw->name, live); |
| return 0; |
| } |
| |
| dead = hw->mix_buf.size - live; |
| hw_free = audio_pcm_hw_get_free(hw); |
| hw_free = hw_free > live ? hw_free - live : 0; |
| frames_out_max = MIN(dead, hw_free); |
| sw_max = st_rate_frames_in(sw->rate, frames_out_max); |
| fe_max = MIN(buf_len / sw->info.bytes_per_frame + sw->resample_buf.pos, |
| sw->resample_buf.size); |
| frames_in_max = MIN(sw_max, fe_max); |
| |
| if (!frames_in_max) { |
| return 0; |
| } |
| |
| if (frames_in_max > sw->resample_buf.pos) { |
| sw->conv(sw->resample_buf.buffer + sw->resample_buf.pos, |
| buf, frames_in_max - sw->resample_buf.pos); |
| if (!AUDIO_MIXENG_BACKEND_GET_CLASS(sw->hw->s)->volume_out) { |
| mixeng_volume(sw->resample_buf.buffer + sw->resample_buf.pos, |
| frames_in_max - sw->resample_buf.pos, &sw->vol); |
| } |
| } |
| |
| audio_pcm_sw_resample_out(sw, frames_in_max, frames_out_max, |
| &total_in, &total_out); |
| |
| sw->total_hw_samples_mixed += total_out; |
| sw->empty = sw->total_hw_samples_mixed == 0; |
| |
| /* |
| * Upsampling may leave one audio frame in the resample buffer. Decrement |
| * total_in by one if there was a leftover frame from the previous resample |
| * pass in the resample buffer. Increment total_in by one if the current |
| * resample pass left one frame in the resample buffer. |
| */ |
| if (frames_in_max - total_in == 1) { |
| /* copy one leftover audio frame to the beginning of the buffer */ |
| *sw->resample_buf.buffer = *(sw->resample_buf.buffer + total_in); |
| total_in += 1 - sw->resample_buf.pos; |
| sw->resample_buf.pos = 1; |
| } else if (total_in >= sw->resample_buf.pos) { |
| total_in -= sw->resample_buf.pos; |
| sw->resample_buf.pos = 0; |
| } |
| |
| trace_audio_sw_write(SW_NAME(sw), buf_len / sw->info.bytes_per_frame, |
| total_in, sw->total_hw_samples_mixed); |
| |
| return total_in * sw->info.bytes_per_frame; |
| } |
| |
| #define DAC |
| #include "audio_template.h" |
| #undef DAC |
| #include "audio_template.h" |
| |
| /* |
| * Timer |
| */ |
| static int audio_is_timer_needed(AudioMixengBackend *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(AudioMixengBackend *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; |
| AudioMixengBackend *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 |
| */ |
| static size_t audio_mixeng_backend_write(AudioBackend *be, SWVoiceOut *sw, |
| void *buf, size_t size) |
| { |
| HWVoiceOut *hw; |
| |
| if (!sw) { |
| /* XXX: Consider options */ |
| return size; |
| } |
| hw = sw->hw; |
| |
| if (!hw->enabled) { |
| warn_report("audio: Writing to disabled voice %s", 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 AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s)->write(hw, buf, size); |
| } |
| } |
| |
| static size_t audio_mixeng_backend_read(AudioBackend *be, |
| SWVoiceIn *sw, void *buf, size_t size) |
| { |
| HWVoiceIn *hw; |
| |
| if (!sw) { |
| /* XXX: Consider options */ |
| return size; |
| } |
| hw = sw->hw; |
| |
| if (!hw->enabled) { |
| warn_report("audio: Reading from disabled voice %s", 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 AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s)->read(hw, buf, size); |
| } |
| |
| } |
| |
| static int audio_mixeng_backend_get_buffer_size_out(AudioBackend *be, SWVoiceOut *sw) |
| { |
| if (!sw) { |
| return 0; |
| } |
| |
| if (audio_get_pdo_out(sw->s->dev)->mixing_engine) { |
| return sw->resample_buf.size * sw->info.bytes_per_frame; |
| } |
| |
| return sw->hw->samples * sw->hw->info.bytes_per_frame; |
| } |
| |
| static void audio_mixeng_backend_set_active_out(AudioBackend *be, SWVoiceOut *sw, |
| bool on) |
| { |
| HWVoiceOut *hw; |
| |
| if (!sw) { |
| return; |
| } |
| |
| hw = sw->hw; |
| if (sw->active != on) { |
| AudioMixengBackend *s = sw->s; |
| SWVoiceOut *temp_sw; |
| SWVoiceCap *sc; |
| |
| if (on) { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(s); |
| |
| hw->pending_disable = 0; |
| if (!hw->enabled) { |
| hw->enabled = true; |
| if (runstate_is_running()) { |
| if (k->enable_out) { |
| k->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; |
| } |
| |
| } |
| |
| static void audio_mixeng_backend_set_active_in(AudioBackend *be, SWVoiceIn *sw, bool on) |
| { |
| HWVoiceIn *hw; |
| |
| if (!sw) { |
| return; |
| } |
| |
| hw = sw->hw; |
| if (sw->active != on) { |
| AudioMixengBackend *s = sw->s; |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(s); |
| SWVoiceIn *temp_sw; |
| |
| if (on) { |
| if (!hw->enabled) { |
| hw->enabled = true; |
| if (runstate_is_running()) { |
| if (k->enable_in) { |
| k->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 = false; |
| if (k->enable_in) { |
| k->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 (live > sw->hw->conv_buf.size) { |
| audio_bug("live=%zu sw->hw->conv_buf.size=%zu", live, |
| sw->hw->conv_buf.size); |
| return 0; |
| } |
| |
| trace_audio_get_avail(SW_NAME(sw), live, st_rate_frames_out(sw->rate, live)); |
| |
| return live; |
| } |
| |
| static size_t audio_get_free(SWVoiceOut *sw) |
| { |
| size_t live, dead; |
| |
| if (!sw) { |
| return 0; |
| } |
| |
| live = sw->total_hw_samples_mixed; |
| |
| if (live > sw->hw->mix_buf.size) { |
| audio_bug("live=%zu sw->hw->mix_buf.size=%zu", live, |
| sw->hw->mix_buf.size); |
| return 0; |
| } |
| |
| dead = sw->hw->mix_buf.size - live; |
| |
| trace_audio_get_free(SW_NAME(sw), live, dead, |
| st_rate_frames_in(sw->rate, dead)); |
| |
| return dead; |
| } |
| |
| 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; |
| size_t rpos2 = rpos; |
| |
| n = samples; |
| while (n) { |
| size_t till_end_of_hw = hw->mix_buf.size - rpos2; |
| size_t to_read = MIN(till_end_of_hw, n); |
| size_t live, frames_in, frames_out; |
| |
| sw->resample_buf.buffer = hw->mix_buf.buffer + rpos2; |
| sw->resample_buf.size = to_read; |
| live = sw->total_hw_samples_mixed; |
| |
| audio_pcm_sw_resample_out(sw, |
| to_read, sw->hw->mix_buf.size - live, |
| &frames_in, &frames_out); |
| |
| sw->total_hw_samples_mixed += frames_out; |
| sw->empty = sw->total_hw_samples_mixed == 0; |
| |
| if (to_read - frames_in) { |
| audio_bug("Could not mix %zu frames into a capture " |
| "buffer, mixed %zu", to_read, frames_in); |
| break; |
| } |
| n -= to_read; |
| rpos2 = (rpos2 + to_read) % hw->mix_buf.size; |
| } |
| } |
| } |
| |
| n = MIN(samples, hw->mix_buf.size - rpos); |
| mixeng_clear(hw->mix_buf.buffer + rpos, n); |
| mixeng_clear(hw->mix_buf.buffer, samples - n); |
| } |
| |
| static size_t audio_pcm_hw_run_out(HWVoiceOut *hw, size_t live) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| size_t clipped = 0; |
| |
| while (live) { |
| size_t size = live * hw->info.bytes_per_frame; |
| size_t decr, proc; |
| void *buf = k->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 = k->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 (k->run_buffer_out) { |
| k->run_buffer_out(hw); |
| } |
| |
| return clipped; |
| } |
| |
| static void audio_run_out(AudioMixengBackend *s) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(s); |
| HWVoiceOut *hw = NULL; |
| SWVoiceOut *sw; |
| |
| while ((hw = audio_pcm_hw_find_any_enabled_out(s, hw))) { |
| size_t played, live, prev_rpos; |
| size_t hw_free = audio_pcm_hw_get_free(hw); |
| int nb_live; |
| |
| if (!audio_get_pdo_out(s->dev)->mixing_engine) { |
| /* there is exactly 1 sw for each hw with no mixeng */ |
| sw = hw->sw_head.lh_first; |
| |
| if (hw->pending_disable) { |
| hw->enabled = false; |
| hw->pending_disable = false; |
| if (k->enable_out) { |
| k->enable_out(hw, false); |
| } |
| } |
| |
| if (sw->active) { |
| sw->callback.fn(sw->callback.opaque, |
| hw_free * sw->info.bytes_per_frame); |
| } |
| |
| if (k->run_buffer_out) { |
| k->run_buffer_out(hw); |
| } |
| |
| continue; |
| } |
| |
| for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { |
| if (sw->active) { |
| size_t sw_free = audio_get_free(sw); |
| size_t free; |
| |
| if (hw_free > sw->total_hw_samples_mixed) { |
| free = st_rate_frames_in(sw->rate, |
| MIN(sw_free, hw_free - sw->total_hw_samples_mixed)); |
| } else { |
| free = 0; |
| } |
| if (free > sw->resample_buf.pos) { |
| free = MIN(free, sw->resample_buf.size) |
| - sw->resample_buf.pos; |
| sw->callback.fn(sw->callback.opaque, |
| free * sw->info.bytes_per_frame); |
| } |
| } |
| } |
| |
| live = audio_pcm_hw_get_live_out (hw, &nb_live); |
| if (!nb_live) { |
| live = 0; |
| } |
| |
| if (live > hw->mix_buf.size) { |
| audio_bug("live=%zu hw->mix_buf.size=%zu", live, hw->mix_buf.size); |
| continue; |
| } |
| |
| if (hw->pending_disable && !nb_live) { |
| SWVoiceCap *sc; |
| |
| trace_audio_out_disable(); |
| hw->enabled = false; |
| hw->pending_disable = false; |
| if (k->enable_out) { |
| k->enable_out(hw, false); |
| } |
| for (sc = hw->cap_head.lh_first; sc; sc = sc->entries.le_next) { |
| sc->sw.active = false; |
| audio_recalc_and_notify_capture (sc->cap); |
| } |
| continue; |
| } |
| |
| if (!live) { |
| if (k->run_buffer_out) { |
| k->run_buffer_out(hw); |
| } |
| continue; |
| } |
| |
| prev_rpos = hw->mix_buf.pos; |
| played = audio_pcm_hw_run_out(hw, live); |
| replay_audio_out(&played); |
| if (hw->mix_buf.pos >= hw->mix_buf.size) { |
| audio_bug("hw->mix_buf.pos=%zu hw->mix_buf.size=%zu played=%zu", |
| hw->mix_buf.pos, hw->mix_buf.size, played); |
| hw->mix_buf.pos = 0; |
| } |
| |
| trace_audio_out_played(played); |
| |
| if (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 (played > sw->total_hw_samples_mixed) { |
| audio_bug("played=%zu sw->total_hw_samples_mixed=%zu", |
| 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 = true; |
| } |
| } |
| } |
| } |
| |
| static size_t audio_pcm_hw_run_in(HWVoiceIn *hw, size_t samples) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| size_t conv = 0; |
| |
| if (k->run_buffer_in) { |
| k->run_buffer_in(hw); |
| } |
| |
| while (samples) { |
| size_t proc; |
| size_t size = samples * hw->info.bytes_per_frame; |
| void *buf = k->get_buffer_in(hw, &size); |
| |
| assert(size % hw->info.bytes_per_frame == 0); |
| if (size == 0) { |
| break; |
| } |
| |
| proc = audio_pcm_hw_conv_in(hw, buf, size / hw->info.bytes_per_frame); |
| |
| samples -= proc; |
| conv += proc; |
| k->put_buffer_in(hw, buf, proc * hw->info.bytes_per_frame); |
| } |
| |
| return conv; |
| } |
| |
| static void audio_run_in(AudioMixengBackend *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; |
| int pos; |
| |
| 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_start(&captured); |
| assert(captured <= hw->conv_buf.size); |
| if (replay_mode == REPLAY_MODE_PLAY) { |
| hw->conv_buf.pos = (hw->conv_buf.pos + captured) % hw->conv_buf.size; |
| } |
| for (pos = (hw->conv_buf.pos - captured + hw->conv_buf.size) % hw->conv_buf.size; |
| pos != hw->conv_buf.pos; |
| pos = (pos + 1) % hw->conv_buf.size) { |
| uint64_t left, right; |
| |
| if (replay_mode == REPLAY_MODE_RECORD) { |
| audio_sample_to_uint64(hw->conv_buf.buffer, pos, &left, &right); |
| } |
| replay_audio_in_sample_lr(&left, &right); |
| if (replay_mode == REPLAY_MODE_PLAY) { |
| audio_sample_from_uint64(hw->conv_buf.buffer, pos, left, right); |
| } |
| } |
| replay_audio_in_finish(); |
| |
| min = audio_pcm_hw_find_min_in (hw); |
| hw->total_samples_captured += captured - min; |
| |
| for (sw = hw->sw_head.lh_first; sw; sw = sw->entries.le_next) { |
| sw->total_hw_samples_acquired -= min; |
| |
| if (sw->active) { |
| size_t sw_avail = audio_get_avail(sw); |
| size_t avail; |
| |
| avail = st_rate_frames_out(sw->rate, sw_avail); |
| if (avail > 0) { |
| avail = MIN(avail, sw->resample_buf.size); |
| sw->callback.fn(sw->callback.opaque, |
| avail * sw->info.bytes_per_frame); |
| } |
| } |
| } |
| } |
| } |
| |
| static void audio_run_capture(AudioMixengBackend *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.buffer + 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 (captured > sw->total_hw_samples_mixed) { |
| audio_bug("captured=%zu sw->total_hw_samples_mixed=%zu", |
| 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(AudioMixengBackend *s, const char *msg) |
| { |
| audio_run_out(s); |
| audio_run_in(s); |
| audio_run_capture(s); |
| |
| if (trace_event_get_state(TRACE_AUDIO_RUN_POLL)) { |
| /* Convert seconds to microseconds for trace event */ |
| int64_t elapsed_us = g_timer_elapsed(s->run_timer, NULL) * MICROSECONDS_PER_SECOND; |
| trace_audio_run_poll(msg, elapsed_us); |
| g_timer_start(s->run_timer); |
| } |
| } |
| |
| void audio_generic_run_buffer_in(HWVoiceIn *hw) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| |
| 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 = k->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) |
| { |
| size_t start; |
| |
| start = audio_ring_posb(hw->pos_emul, hw->pending_emul, hw->size_emul); |
| assert(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; |
| } |
| |
| size_t audio_generic_buffer_get_free(HWVoiceOut *hw) |
| { |
| if (hw->buf_emul) { |
| return hw->size_emul - hw->pending_emul; |
| } else { |
| return hw->samples * hw->info.bytes_per_frame; |
| } |
| } |
| |
| void audio_generic_run_buffer_out(HWVoiceOut *hw) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| |
| while (hw->pending_emul) { |
| size_t write_len, written, start; |
| |
| start = audio_ring_posb(hw->pos_emul, hw->pending_emul, hw->size_emul); |
| assert(start < hw->size_emul); |
| |
| write_len = MIN(hw->pending_emul, hw->size_emul - start); |
| |
| written = k->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) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| size_t total = 0; |
| |
| if (k->buffer_get_free) { |
| size_t free = k->buffer_get_free(hw); |
| |
| size = MIN(size, free); |
| } |
| |
| while (total < size) { |
| size_t dst_size = size - total; |
| size_t copy_size, proc; |
| void *dst = k->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 = k->put_buffer_out(hw, dst, copy_size); |
| total += proc; |
| |
| if (proc == 0 || proc < copy_size) { |
| break; |
| } |
| } |
| |
| return total; |
| } |
| |
| size_t audio_generic_read(HWVoiceIn *hw, void *buf, size_t size) |
| { |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| size_t total = 0; |
| |
| if (k->run_buffer_in) { |
| k->run_buffer_in(hw); |
| } |
| |
| while (total < size) { |
| size_t src_size = size - total; |
| void *src = k->get_buffer_in(hw, &src_size); |
| |
| if (src_size == 0) { |
| break; |
| } |
| |
| memcpy((char *)buf + total, src, src_size); |
| k->put_buffer_in(hw, src, src_size); |
| total += src_size; |
| } |
| |
| return total; |
| } |
| |
| static bool audio_mixeng_backend_realize(AudioBackend *abe, |
| Audiodev *dev, Error **errp) |
| { |
| AudioMixengBackend *be = AUDIO_MIXENG_BACKEND(abe); |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(be); |
| |
| be->dev = dev; |
| if (!k->get_buffer_in) { |
| k->get_buffer_in = audio_generic_get_buffer_in; |
| k->put_buffer_in = audio_generic_put_buffer_in; |
| } |
| if (!k->get_buffer_out) { |
| k->get_buffer_out = audio_generic_get_buffer_out; |
| k->put_buffer_out = audio_generic_put_buffer_out; |
| } |
| |
| audio_init_nb_voices_out(be, k, 1); |
| audio_init_nb_voices_in(be, k, 0); |
| |
| if (be->dev->timer_period <= 0) { |
| be->period_ticks = 1; |
| } else { |
| be->period_ticks = be->dev->timer_period * (int64_t)SCALE_US; |
| } |
| |
| return true; |
| } |
| |
| static void audio_vm_change_state_handler (void *opaque, bool running, |
| RunState state) |
| { |
| AudioMixengBackend *s = opaque; |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(s); |
| HWVoiceOut *hwo = NULL; |
| HWVoiceIn *hwi = NULL; |
| |
| while ((hwo = audio_pcm_hw_find_any_enabled_out(s, hwo))) { |
| if (k->enable_out) { |
| k->enable_out(hwo, running); |
| } |
| } |
| |
| while ((hwi = audio_pcm_hw_find_any_enabled_in(s, hwi))) { |
| if (k->enable_in) { |
| k->enable_in(hwi, running); |
| } |
| } |
| audio_reset_timer (s); |
| } |
| |
| static const VMStateDescription vmstate_audio; |
| |
| static const char *audio_mixeng_backend_get_id(AudioBackend *be) |
| { |
| return AUDIO_MIXENG_BACKEND(be)->dev->id; |
| } |
| |
| static CaptureVoiceOut *audio_mixeng_backend_add_capture( |
| AudioBackend *be, |
| const struct audsettings *as, |
| const struct audio_capture_ops *ops, |
| void *cb_opaque); |
| |
| static void audio_mixeng_backend_del_capture( |
| AudioBackend *be, |
| CaptureVoiceOut *cap, |
| void *cb_opaque); |
| |
| static void audio_mixeng_backend_set_volume_out(AudioBackend *be, SWVoiceOut *sw, |
| Volume *vol); |
| static void audio_mixeng_backend_set_volume_in(AudioBackend *be, SWVoiceIn *sw, |
| Volume *vol); |
| |
| static void audio_mixeng_backend_class_init(ObjectClass *klass, const void *data) |
| { |
| AudioBackendClass *be = AUDIO_BACKEND_CLASS(klass); |
| |
| be->realize = audio_mixeng_backend_realize; |
| be->get_id = audio_mixeng_backend_get_id; |
| be->open_in = audio_mixeng_backend_open_in; |
| be->open_out = audio_mixeng_backend_open_out; |
| be->close_in = audio_mixeng_backend_close_in; |
| be->close_out = audio_mixeng_backend_close_out; |
| be->is_active_out = audio_mixeng_backend_is_active_out; |
| be->is_active_in = audio_mixeng_backend_is_active_in; |
| be->set_active_out = audio_mixeng_backend_set_active_out; |
| be->set_active_in = audio_mixeng_backend_set_active_in; |
| be->set_volume_out = audio_mixeng_backend_set_volume_out; |
| be->set_volume_in = audio_mixeng_backend_set_volume_in; |
| be->read = audio_mixeng_backend_read; |
| be->write = audio_mixeng_backend_write; |
| be->get_buffer_size_out = audio_mixeng_backend_get_buffer_size_out; |
| be->add_capture = audio_mixeng_backend_add_capture; |
| be->del_capture = audio_mixeng_backend_del_capture; |
| } |
| |
| static void audio_mixeng_backend_init(Object *obj) |
| { |
| AudioMixengBackend *s = AUDIO_MIXENG_BACKEND(obj); |
| |
| QLIST_INIT(&s->hw_head_out); |
| QLIST_INIT(&s->hw_head_in); |
| QLIST_INIT(&s->cap_head); |
| s->ts = timer_new_ns(QEMU_CLOCK_VIRTUAL, audio_timer, s); |
| s->run_timer = g_timer_new(); |
| |
| s->vmse = qemu_add_vm_change_state_handler(audio_vm_change_state_handler, s); |
| assert(s->vmse != NULL); |
| |
| vmstate_register_any(NULL, &vmstate_audio, s); |
| } |
| |
| static void audio_mixeng_backend_finalize(Object *obj) |
| { |
| AudioMixengBackend *s = AUDIO_MIXENG_BACKEND(obj); |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(s); |
| HWVoiceOut *hwo, *hwon; |
| HWVoiceIn *hwi, *hwin; |
| |
| QLIST_FOREACH_SAFE(hwo, &s->hw_head_out, entries, hwon) { |
| SWVoiceCap *sc; |
| |
| if (hwo->enabled && k->enable_out) { |
| k->enable_out(hwo, false); |
| } |
| k->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 && k->enable_in) { |
| k->enable_in(hwi, false); |
| } |
| k->fini_in(hwi); |
| QLIST_REMOVE(hwi, entries); |
| } |
| |
| if (s->dev) { |
| qapi_free_Audiodev(s->dev); |
| s->dev = NULL; |
| } |
| |
| if (s->ts) { |
| timer_free(s->ts); |
| s->ts = NULL; |
| } |
| |
| g_clear_pointer(&s->run_timer, g_timer_destroy); |
| |
| if (s->vmse) { |
| qemu_del_vm_change_state_handler(s->vmse); |
| s->vmse = NULL; |
| } |
| |
| vmstate_unregister(NULL, &vmstate_audio, 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 = (const VMStateField[]) { |
| VMSTATE_END_OF_LIST() |
| } |
| }; |
| |
| static CaptureVoiceOut *audio_mixeng_backend_add_capture( |
| AudioBackend *be, |
| const struct audsettings *as, |
| const struct audio_capture_ops *ops, |
| void *cb_opaque) |
| { |
| AudioMixengBackend *s = AUDIO_MIXENG_BACKEND(be); |
| CaptureVoiceOut *cap; |
| struct capture_callback *cb; |
| |
| if (!s) { |
| error_report("Capturing without setting an audiodev is not supported"); |
| abort(); |
| } |
| |
| if (!audio_get_pdo_out(s->dev)->mixing_engine) { |
| error_report("audio: Can't capture with mixeng disabled"); |
| return NULL; |
| } |
| |
| if (audio_validate_settings(as)) { |
| g_autofree char *str = audsettings_to_string(as); |
| error_report("audio: Invalid audio settings when trying to add capture: %s", str); |
| 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); |
| } else { |
| HWVoiceOut *hw; |
| |
| 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 (audio_format_is_float(hw->info.af)) { |
| hw->clip = mixeng_clip_float[hw->info.nchannels == 2] |
| [hw->info.swap_endianness]; |
| } else { |
| hw->clip = mixeng_clip |
| [hw->info.nchannels == 2] |
| [audio_format_is_signed(hw->info.af)] |
| [hw->info.swap_endianness] |
| [audio_format_to_index(hw->info.af)]; |
| } |
| |
| 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; |
| } |
| |
| static void audio_mixeng_backend_del_capture( |
| AudioBackend *be, |
| 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; |
| |
| trace_audio_capture_free_sw(sw->name); |
| g_clear_pointer(&sw->name, g_free); |
| 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.buffer); |
| g_free (cap->buf); |
| g_free (cap); |
| } |
| return; |
| } |
| } |
| } |
| |
| static void audio_mixeng_backend_set_volume_out(AudioBackend *be, SWVoiceOut *sw, |
| Volume *vol) |
| { |
| if (sw) { |
| HWVoiceOut *hw = sw->hw; |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| |
| 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 (k->volume_out) { |
| k->volume_out(hw, vol); |
| } |
| } |
| } |
| |
| static void audio_mixeng_backend_set_volume_in(AudioBackend *be, SWVoiceIn *sw, |
| Volume *vol) |
| { |
| if (sw) { |
| HWVoiceIn *hw = sw->hw; |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_GET_CLASS(hw->s); |
| |
| 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 (k->volume_in) { |
| k->volume_in(hw, vol); |
| } |
| } |
| } |
| |
| audsettings audiodev_to_audsettings(AudiodevPerDirectionOptions *pdo) |
| { |
| return (audsettings) { |
| .freq = pdo->frequency, |
| .nchannels = pdo->channels, |
| .fmt = pdo->format, |
| .big_endian = HOST_BIG_ENDIAN, |
| }; |
| } |
| |
| /* 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) * audio_format_bits(as->fmt) / 8; |
| } |
| |
| 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_peek_bytes(RateCtl *rate, struct audio_pcm_info *info) |
| { |
| int64_t now; |
| int64_t ticks; |
| int64_t bytes; |
| int64_t frames; |
| |
| now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); |
| ticks = now - rate->start_ticks; |
| bytes = muldiv64(ticks, info->bytes_per_second, NANOSECONDS_PER_SECOND); |
| frames = (bytes - rate->bytes_sent) / info->bytes_per_frame; |
| rate->peeked_frames = frames; |
| |
| return frames < 0 ? 0 : frames * info->bytes_per_frame; |
| } |
| |
| void audio_rate_add_bytes(RateCtl *rate, size_t bytes_used) |
| { |
| if (rate->peeked_frames < 0 || rate->peeked_frames > 65536) { |
| trace_audio_rate_reset(rate->peeked_frames); |
| audio_rate_start(rate); |
| } |
| |
| rate->bytes_sent += bytes_used; |
| } |
| |
| size_t audio_rate_get_bytes(RateCtl *rate, struct audio_pcm_info *info, |
| size_t bytes_avail) |
| { |
| size_t bytes; |
| |
| bytes = audio_rate_peek_bytes(rate, info); |
| bytes = MIN(bytes, bytes_avail); |
| audio_rate_add_bytes(rate, bytes); |
| |
| return bytes; |
| } |
| |
| static const TypeInfo audio_types[] = { |
| { |
| .name = TYPE_AUDIO_MIXENG_BACKEND, |
| .parent = TYPE_AUDIO_BACKEND, |
| .instance_size = sizeof(AudioMixengBackend), |
| .instance_init = audio_mixeng_backend_init, |
| .instance_finalize = audio_mixeng_backend_finalize, |
| .class_size = sizeof(AudioMixengBackendClass), |
| .class_init = audio_mixeng_backend_class_init, |
| }, |
| }; |
| |
| DEFINE_TYPES(audio_types) |