| /* SPDX-License-Identifier: MIT */ |
| |
| #include "qemu/osdep.h" |
| #include "qemu/audio.h" |
| #include "qemu/help_option.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 "system/system.h" |
| |
| /* 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[] = { |
| #ifdef CONFIG_GIO |
| "dbus", |
| #endif |
| "spice", |
| CONFIG_AUDIO_DRIVERS |
| "none", |
| NULL |
| }; |
| |
| typedef struct AudiodevListEntry { |
| Audiodev *dev; |
| QSIMPLEQ_ENTRY(AudiodevListEntry) next; |
| } AudiodevListEntry; |
| |
| typedef QSIMPLEQ_HEAD(, AudiodevListEntry) AudiodevListHead; |
| |
| static AudiodevListHead audiodevs = |
| QSIMPLEQ_HEAD_INITIALIZER(audiodevs); |
| static AudiodevListHead default_audiodevs = |
| QSIMPLEQ_HEAD_INITIALIZER(default_audiodevs); |
| |
| static AudioBackendClass *audio_be_class_by_name(const char *name) |
| { |
| g_autofree char *tname = g_strconcat("audio-", name, NULL); |
| ObjectClass *oc = module_object_class_by_name(tname); |
| |
| if (!oc || !object_class_dynamic_cast(oc, TYPE_AUDIO_BACKEND)) { |
| return NULL; |
| } |
| |
| return AUDIO_BACKEND_CLASS(oc); |
| } |
| |
| static AudioBackend *default_audio_be; |
| |
| static Object *get_audiodevs_root(void) |
| { |
| return object_get_container("audiodevs"); |
| } |
| |
| void audio_cleanup(void) |
| { |
| default_audio_be = NULL; |
| |
| object_unparent(get_audiodevs_root()); |
| } |
| |
| void audio_create_default_audiodevs(void) |
| { |
| for (int i = 0; audio_prio_list[i]; i++) { |
| if (audio_be_class_by_name(audio_prio_list[i]) != NULL) { |
| QDict *dict = qdict_new(); |
| Audiodev *dev = NULL; |
| Visitor *v; |
| |
| qdict_put_str(dict, "driver", audio_prio_list[i]); |
| qdict_put_str(dict, "id", "#default"); |
| |
| v = qobject_input_visitor_new_keyval(QOBJECT(dict)); |
| qobject_unref(dict); |
| visit_type_Audiodev(v, NULL, &dev, &error_fatal); |
| visit_free(v); |
| |
| audio_add_default_audiodev(dev, &error_abort); |
| } |
| } |
| } |
| |
| /* |
| * 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 AudioBackend *audio_init(Audiodev *dev, Error **errp) |
| { |
| AudioBackend *be; |
| |
| if (dev) { |
| be = audio_be_new(dev, errp); |
| if (!be) { |
| return NULL; |
| } |
| } else { |
| assert(!default_audio_be); |
| for (;;) { |
| AudiodevListEntry *e = QSIMPLEQ_FIRST(&default_audiodevs); |
| if (!e) { |
| error_setg(errp, "no default audio driver available"); |
| return NULL; |
| } |
| QSIMPLEQ_REMOVE_HEAD(&default_audiodevs, next); |
| be = audio_be_new(e->dev, NULL); |
| g_free(e); |
| if (be) { |
| break; |
| } |
| } |
| } |
| |
| if (!object_property_try_add_child(get_audiodevs_root(), |
| audio_be_get_id(be), OBJECT(be), errp)) { |
| object_unref(be); |
| return NULL; |
| } |
| object_unref(be); |
| return be; |
| } |
| |
| AudioBackend *audio_get_default_audio_be(Error **errp) |
| { |
| if (!default_audio_be) { |
| default_audio_be = audio_init(NULL, errp); |
| if (!default_audio_be) { |
| if (!QSIMPLEQ_EMPTY(&audiodevs)) { |
| error_append_hint(errp, "Perhaps you wanted to use -audio or set audiodev=%s?\n", |
| QSIMPLEQ_FIRST(&audiodevs)->dev->id); |
| } |
| } |
| } |
| |
| return default_audio_be; |
| } |
| |
| void audio_help(void) |
| { |
| int i; |
| |
| printf("Available audio drivers:\n"); |
| |
| for (i = 0; i < AUDIODEV_DRIVER__MAX; i++) { |
| const char *name = AudiodevDriver_str(i); |
| AudioBackendClass *be = audio_be_class_by_name(name); |
| |
| if (be) { |
| printf("%s\n", name); |
| } |
| } |
| } |
| |
| void audio_parse_option(const char *opt) |
| { |
| Audiodev *dev = NULL; |
| |
| if (is_help_option(opt)) { |
| audio_help(); |
| exit(EXIT_SUCCESS); |
| } |
| Visitor *v = qobject_input_visitor_new_str(opt, "driver", &error_fatal); |
| visit_type_Audiodev(v, NULL, &dev, &error_fatal); |
| visit_free(v); |
| |
| audio_add_audiodev(dev); |
| } |
| |
| static void audio_create_pdos(Audiodev *dev) |
| { |
| switch (dev->driver) { |
| #define CASE(DRIVER, driver, pdo_name) \ |
| case AUDIODEV_DRIVER_##DRIVER: \ |
| if (!dev->u.driver.in) { \ |
| dev->u.driver.in = g_malloc0( \ |
| sizeof(Audiodev##pdo_name##PerDirectionOptions)); \ |
| } \ |
| if (!dev->u.driver.out) { \ |
| dev->u.driver.out = g_malloc0( \ |
| sizeof(Audiodev##pdo_name##PerDirectionOptions)); \ |
| } \ |
| break |
| |
| CASE(NONE, none, ); |
| #ifdef CONFIG_AUDIO_ALSA |
| CASE(ALSA, alsa, Alsa); |
| #endif |
| #ifdef CONFIG_AUDIO_COREAUDIO |
| CASE(COREAUDIO, coreaudio, Coreaudio); |
| #endif |
| #ifdef CONFIG_DBUS_DISPLAY |
| CASE(DBUS, dbus, ); |
| #endif |
| #ifdef CONFIG_AUDIO_DSOUND |
| CASE(DSOUND, dsound, ); |
| #endif |
| #ifdef CONFIG_AUDIO_JACK |
| CASE(JACK, jack, Jack); |
| #endif |
| #ifdef CONFIG_AUDIO_OSS |
| CASE(OSS, oss, Oss); |
| #endif |
| #ifdef CONFIG_AUDIO_PA |
| CASE(PA, pa, Pa); |
| #endif |
| #ifdef CONFIG_AUDIO_PIPEWIRE |
| CASE(PIPEWIRE, pipewire, Pipewire); |
| #endif |
| #ifdef CONFIG_AUDIO_SDL |
| CASE(SDL, sdl, Sdl); |
| #endif |
| #ifdef CONFIG_AUDIO_SNDIO |
| CASE(SNDIO, sndio, ); |
| #endif |
| #ifdef CONFIG_SPICE |
| CASE(SPICE, spice, ); |
| #endif |
| 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 AudiodevPerDirectionOptions *audio_get_pdo_out(Audiodev *dev) |
| { |
| switch (dev->driver) { |
| case AUDIODEV_DRIVER_NONE: |
| return dev->u.none.out; |
| #ifdef CONFIG_AUDIO_ALSA |
| case AUDIODEV_DRIVER_ALSA: |
| return qapi_AudiodevAlsaPerDirectionOptions_base(dev->u.alsa.out); |
| #endif |
| #ifdef CONFIG_AUDIO_COREAUDIO |
| case AUDIODEV_DRIVER_COREAUDIO: |
| return qapi_AudiodevCoreaudioPerDirectionOptions_base( |
| dev->u.coreaudio.out); |
| #endif |
| #ifdef CONFIG_DBUS_DISPLAY |
| case AUDIODEV_DRIVER_DBUS: |
| return dev->u.dbus.out; |
| #endif |
| #ifdef CONFIG_AUDIO_DSOUND |
| case AUDIODEV_DRIVER_DSOUND: |
| return dev->u.dsound.out; |
| #endif |
| #ifdef CONFIG_AUDIO_JACK |
| case AUDIODEV_DRIVER_JACK: |
| return qapi_AudiodevJackPerDirectionOptions_base(dev->u.jack.out); |
| #endif |
| #ifdef CONFIG_AUDIO_OSS |
| case AUDIODEV_DRIVER_OSS: |
| return qapi_AudiodevOssPerDirectionOptions_base(dev->u.oss.out); |
| #endif |
| #ifdef CONFIG_AUDIO_PA |
| case AUDIODEV_DRIVER_PA: |
| return qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.out); |
| #endif |
| #ifdef CONFIG_AUDIO_PIPEWIRE |
| case AUDIODEV_DRIVER_PIPEWIRE: |
| return qapi_AudiodevPipewirePerDirectionOptions_base(dev->u.pipewire.out); |
| #endif |
| #ifdef CONFIG_AUDIO_SDL |
| case AUDIODEV_DRIVER_SDL: |
| return qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.out); |
| #endif |
| #ifdef CONFIG_AUDIO_SNDIO |
| case AUDIODEV_DRIVER_SNDIO: |
| return dev->u.sndio.out; |
| #endif |
| #ifdef CONFIG_SPICE |
| case AUDIODEV_DRIVER_SPICE: |
| return dev->u.spice.out; |
| #endif |
| case AUDIODEV_DRIVER_WAV: |
| return dev->u.wav.out; |
| |
| case AUDIODEV_DRIVER__MAX: |
| break; |
| } |
| abort(); |
| } |
| |
| static AudiodevPerDirectionOptions *audio_get_pdo_in(Audiodev *dev) |
| { |
| switch (dev->driver) { |
| case AUDIODEV_DRIVER_NONE: |
| return dev->u.none.in; |
| #ifdef CONFIG_AUDIO_ALSA |
| case AUDIODEV_DRIVER_ALSA: |
| return qapi_AudiodevAlsaPerDirectionOptions_base(dev->u.alsa.in); |
| #endif |
| #ifdef CONFIG_AUDIO_COREAUDIO |
| case AUDIODEV_DRIVER_COREAUDIO: |
| return qapi_AudiodevCoreaudioPerDirectionOptions_base( |
| dev->u.coreaudio.in); |
| #endif |
| #ifdef CONFIG_DBUS_DISPLAY |
| case AUDIODEV_DRIVER_DBUS: |
| return dev->u.dbus.in; |
| #endif |
| #ifdef CONFIG_AUDIO_DSOUND |
| case AUDIODEV_DRIVER_DSOUND: |
| return dev->u.dsound.in; |
| #endif |
| #ifdef CONFIG_AUDIO_JACK |
| case AUDIODEV_DRIVER_JACK: |
| return qapi_AudiodevJackPerDirectionOptions_base(dev->u.jack.in); |
| #endif |
| #ifdef CONFIG_AUDIO_OSS |
| case AUDIODEV_DRIVER_OSS: |
| return qapi_AudiodevOssPerDirectionOptions_base(dev->u.oss.in); |
| #endif |
| #ifdef CONFIG_AUDIO_PA |
| case AUDIODEV_DRIVER_PA: |
| return qapi_AudiodevPaPerDirectionOptions_base(dev->u.pa.in); |
| #endif |
| #ifdef CONFIG_AUDIO_PIPEWIRE |
| case AUDIODEV_DRIVER_PIPEWIRE: |
| return qapi_AudiodevPipewirePerDirectionOptions_base(dev->u.pipewire.in); |
| #endif |
| #ifdef CONFIG_AUDIO_SDL |
| case AUDIODEV_DRIVER_SDL: |
| return qapi_AudiodevSdlPerDirectionOptions_base(dev->u.sdl.in); |
| #endif |
| #ifdef CONFIG_AUDIO_SNDIO |
| case AUDIODEV_DRIVER_SNDIO: |
| return dev->u.sndio.in; |
| #endif |
| #ifdef CONFIG_SPICE |
| case AUDIODEV_DRIVER_SPICE: |
| return dev->u.spice.in; |
| #endif |
| case AUDIODEV_DRIVER_WAV: |
| return dev->u.wav.in; |
| |
| case AUDIODEV_DRIVER__MAX: |
| break; |
| } |
| abort(); |
| } |
| |
| 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_add_audiodev(Audiodev *dev) |
| { |
| AudiodevListEntry *e; |
| |
| audio_validate_opts(dev, &error_fatal); |
| |
| e = g_new0(AudiodevListEntry, 1); |
| e->dev = dev; |
| QSIMPLEQ_INSERT_TAIL(&audiodevs, e, next); |
| } |
| |
| void audio_add_default_audiodev(Audiodev *dev, Error **errp) |
| { |
| AudiodevListEntry *e; |
| |
| audio_validate_opts(dev, errp); |
| |
| e = g_new0(AudiodevListEntry, 1); |
| e->dev = dev; |
| QSIMPLEQ_INSERT_TAIL(&default_audiodevs, e, next); |
| } |
| |
| void audio_init_audiodevs(void) |
| { |
| AudiodevListEntry *e; |
| |
| QSIMPLEQ_FOREACH(e, &audiodevs, next) { |
| audio_init(QAPI_CLONE(Audiodev, e->dev), &error_fatal); |
| } |
| } |
| |
| AudioBackend *audio_be_by_name(const char *name, Error **errp) |
| { |
| Object *obj = object_resolve_path_component(get_audiodevs_root(), name); |
| |
| if (!obj) { |
| error_setg(errp, "audiodev '%s' not found", name); |
| return NULL; |
| } else { |
| return AUDIO_BACKEND(obj); |
| } |
| } |
| |
| const char *audio_application_name(void) |
| { |
| const char *vm_name; |
| |
| vm_name = qemu_get_vm_name(); |
| return vm_name ? vm_name : "qemu"; |
| } |
| |
| AudiodevList *qmp_query_audiodevs(Error **errp) |
| { |
| AudiodevList *ret = NULL; |
| AudiodevListEntry *e; |
| QSIMPLEQ_FOREACH(e, &audiodevs, next) { |
| QAPI_LIST_PREPEND(ret, QAPI_CLONE(Audiodev, e->dev)); |
| } |
| return ret; |
| } |