diff -ru a/audio/coreaudio.m b/audio/coreaudio.m --- a/audio/coreaudio.m 2026-10-03 10:21:53 +++ b/audio/coreaudio.m 2026-10-03 10:21:53 @@ -24,6 +24,7 @@ #include "qemu/osdep.h" #include +#include #include /* pthread_X */ #include "qemu/main-loop.h" @@ -650,14 +651,189 @@ update_out_device_playback_state(core); } + +/* + * Capture (microphone) support. + * + * Uses an AudioQueue on the default input device. The queue converts the + * device format to the format the guest asked for (32-bit float at the + * guest's rate and channel count), and calls in_queue_callback on its own + * thread with each filled buffer. The callback copies the data into the + * HWVoiceIn ring buffer (buf_emul), like the SDL backend does. + * Opening the input triggers the macOS microphone permission prompt for the + * responsible app the first time. + */ +typedef struct coreaudioVoiceIn { + HWVoiceIn hw; + pthread_mutex_t buf_mutex; + AudioQueueRef queue; + bool running; +} CoreaudioVoiceIn; + +#define COREAUDIO_IN_BUFFERS 3 + +/* called on the AudioQueue thread without BQL; only takes buf_mutex */ +static void in_queue_callback(void *user, AudioQueueRef queue, + AudioQueueBufferRef buffer, + const AudioTimeStamp *start_time, + UInt32 packets, + const AudioStreamPacketDescription *descs) +{ + CoreaudioVoiceIn *core = user; + HWVoiceIn *hw = &core->hw; + const uint8_t *src = buffer->mAudioData; + size_t len = buffer->mAudioDataByteSize; + + pthread_mutex_lock(&core->buf_mutex); + while (len && hw->pending_emul < hw->size_emul) { + size_t read_len = MIN(len, MIN(hw->size_emul - hw->pos_emul, + hw->size_emul - hw->pending_emul)); + + memcpy((uint8_t *)hw->buf_emul + hw->pos_emul, src, read_len); + hw->pending_emul += read_len; + hw->pos_emul = (hw->pos_emul + read_len) % hw->size_emul; + len -= read_len; + src += read_len; + } + pthread_mutex_unlock(&core->buf_mutex); + + if (core->running) { + AudioQueueEnqueueBuffer(queue, buffer, 0, NULL); + } +} + +#define COREAUDIO_IN_WRAPPER_FUNC(name, ret_type, args_decl, args) \ + static ret_type glue(coreaudio_in_, name)args_decl \ + { \ + CoreaudioVoiceIn *core = (CoreaudioVoiceIn *)hw; \ + ret_type ret; \ + \ + pthread_mutex_lock(&core->buf_mutex); \ + ret = glue(audio_generic_, name)args; \ + pthread_mutex_unlock(&core->buf_mutex); \ + return ret; \ + } +COREAUDIO_IN_WRAPPER_FUNC(read, size_t, (HWVoiceIn *hw, void *buf, size_t size), + (hw, buf, size)) +COREAUDIO_IN_WRAPPER_FUNC(get_buffer_in, void *, (HWVoiceIn *hw, size_t *size), + (hw, size)) +#undef COREAUDIO_IN_WRAPPER_FUNC + +static void coreaudio_in_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size) +{ + CoreaudioVoiceIn *core = (CoreaudioVoiceIn *)hw; + + pthread_mutex_lock(&core->buf_mutex); + audio_generic_put_buffer_in(hw, buf, size); + pthread_mutex_unlock(&core->buf_mutex); +} + +static int coreaudio_init_in(HWVoiceIn *hw, struct audsettings *as) +{ + CoreaudioVoiceIn *core = (CoreaudioVoiceIn *)hw; + Audiodev *dev = hw->s->dev; + AudiodevCoreaudioPerDirectionOptions *cpdo = dev->u.coreaudio.in; + struct audsettings obt_as = *as; + AudioStreamBasicDescription format; + OSStatus status; + int frames, i, err; + + err = pthread_mutex_init(&core->buf_mutex, NULL); + if (err) { + error_report("coreaudio: Could not create mutex: %s", strerror(err)); + return -1; + } + + obt_as.fmt = AUDIO_FORMAT_F32; + audio_pcm_init_info(&hw->info, &obt_as); + + frames = audio_buffer_frames( + qapi_AudiodevCoreaudioPerDirectionOptions_base(cpdo), &obt_as, 11610); + hw->samples = (cpdo->has_buffer_count ? cpdo->buffer_count : 4) * frames; + hw->size_emul = hw->samples * hw->info.bytes_per_frame; + hw->buf_emul = g_malloc0(hw->size_emul); + hw->pos_emul = hw->pending_emul = 0; + + format = (AudioStreamBasicDescription) { + .mSampleRate = hw->info.freq, + .mFormatID = kAudioFormatLinearPCM, + .mFormatFlags = kLinearPCMFormatFlagIsFloat | + kLinearPCMFormatFlagIsPacked, + .mBytesPerPacket = hw->info.bytes_per_frame, + .mFramesPerPacket = 1, + .mBytesPerFrame = hw->info.bytes_per_frame, + .mChannelsPerFrame = hw->info.nchannels, + .mBitsPerChannel = 32, + }; + + status = AudioQueueNewInput(&format, in_queue_callback, core, + NULL, NULL, 0, &core->queue); + if (status != noErr) { + coreaudio_logerr2(status, "capture", "Could not create input queue"); + core->queue = NULL; + return -1; + } + + for (i = 0; i < COREAUDIO_IN_BUFFERS; i++) { + AudioQueueBufferRef buffer; + + status = AudioQueueAllocateBuffer(core->queue, + frames * hw->info.bytes_per_frame, + &buffer); + if (status != noErr) { + coreaudio_logerr2(status, "capture", "Could not allocate buffer"); + break; + } + AudioQueueEnqueueBuffer(core->queue, buffer, 0, NULL); + } + core->running = false; + return 0; +} + +static void coreaudio_fini_in(HWVoiceIn *hw) +{ + CoreaudioVoiceIn *core = (CoreaudioVoiceIn *)hw; + + if (core->queue) { + core->running = false; + AudioQueueStop(core->queue, true); + AudioQueueDispose(core->queue, true); + core->queue = NULL; + } + pthread_mutex_destroy(&core->buf_mutex); +} + +static void coreaudio_enable_in(HWVoiceIn *hw, bool enable) +{ + CoreaudioVoiceIn *core = (CoreaudioVoiceIn *)hw; + OSStatus status; + + if (!core->queue || core->running == enable) { + return; + } + core->running = enable; + if (enable) { + status = AudioQueueStart(core->queue, NULL); + if (status != noErr) { + core->running = false; + coreaudio_logerr(status, "Could not start capture"); + } + } else { + status = AudioQueuePause(core->queue); + if (status != noErr) { + coreaudio_logerr(status, "Could not pause capture"); + } + } +} + static void audio_coreaudio_class_init(ObjectClass *klass, const void *data) { AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_CLASS(klass); k->max_voices_out = 1; - k->max_voices_in = 0; + k->max_voices_in = 1; k->voice_size_out = sizeof(CoreaudioVoiceOut); - k->voice_size_in = 0; + k->voice_size_in = sizeof(CoreaudioVoiceIn); k->init_out = coreaudio_init_out; k->fini_out = coreaudio_fini_out; @@ -670,6 +846,14 @@ /* wrapper for audio_generic_put_buffer_out */ k->put_buffer_out = coreaudio_put_buffer_out; k->enable_out = coreaudio_enable_out; + + k->init_in = coreaudio_init_in; + k->fini_in = coreaudio_fini_in; + /* wrappers for audio_generic_read / get_buffer_in / put_buffer_in */ + k->read = coreaudio_in_read; + k->get_buffer_in = coreaudio_in_get_buffer_in; + k->put_buffer_in = coreaudio_in_put_buffer_in; + k->enable_in = coreaudio_enable_in; } static const TypeInfo audio_types[] = { diff -ru a/meson.build b/meson.build --- a/meson.build 2026-10-03 10:21:53 +++ b/meson.build 2026-10-03 10:21:53 @@ -1764,7 +1764,7 @@ coreaudio = not_found if not get_option('coreaudio').auto() or (host_os == 'darwin' and have_system) - coreaudio = dependency('appleframeworks', modules: 'CoreAudio', + coreaudio = dependency('appleframeworks', modules: ['CoreAudio', 'AudioToolbox'], required: get_option('coreaudio')) endif